diff --git a/.github/workflows/brcb-hard-profile-ci.yml b/.github/workflows/brcb-hard-profile-ci.yml new file mode 100644 index 000000000..2c7bd3d8e --- /dev/null +++ b/.github/workflows/brcb-hard-profile-ci.yml @@ -0,0 +1,90 @@ +name: BRCB Hard Profile CI + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + brcb-hard-profile: + name: BRCB Hard / ${{ matrix.compiler }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + + steps: + - uses: actions/checkout@v4 + + - name: Select compiler + shell: bash + run: | + if [ "${{ matrix.compiler }}" = "clang" ]; then + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + else + echo "CC=gcc" >> "$GITHUB_ENV" + echo "CXX=g++" >> "$GITHUB_ENV" + fi + + - name: Configure BRCB hard profile + run: cmake -S embedded/brcb_hard_profile -B build-brcb-hard + + - name: Build BRCB hard profile + run: cmake --build build-brcb-hard --parallel + + - name: Run BRCB smokes + run: >- + ctest --test-dir build-brcb-hard + --output-on-failure + --output-junit brcb-ctest.xml + + - name: Prove exception runtime is absent + shell: bash + run: | + set -euo pipefail + for binary in \ + build-brcb-hard/ariec61850_hard_mms_brcb_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_control_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_objects_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_lifecycle_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_connection_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_recovery_v2_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_persistence_smoke; do + if nm -C "$binary" | grep -E '__cxa_throw|__cxa_begin_catch|__cxa_end_catch|__gxx_personality_v0'; then + echo "exception runtime symbol found in $binary" >&2 + exit 1 + fi + done + echo 'NO_EXCEPTION_RUNTIME=PASS' + + - name: Report footprint + shell: bash + run: | + set -euo pipefail + { + echo "compiler=${{ matrix.compiler }}" + size \ + build-brcb-hard/ariec61850_hard_mms_brcb_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_control_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_objects_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_lifecycle_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_connection_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_recovery_v2_smoke \ + build-brcb-hard/ariec61850_hard_mms_brcb_persistence_smoke + } | tee build-brcb-hard/brcb-footprint.txt + + - name: Upload BRCB evidence + uses: actions/upload-artifact@v4 + with: + name: brcb-hard-${{ matrix.compiler }}-${{ github.event.pull_request.head.sha || github.sha }} + if-no-files-found: error + retention-days: 14 + path: | + build-brcb-hard/brcb-ctest.xml + build-brcb-hard/brcb-footprint.txt diff --git a/.github/workflows/mms-r12-server-ci.yml b/.github/workflows/mms-r12-server-ci.yml new file mode 100644 index 000000000..23f89af9d --- /dev/null +++ b/.github/workflows/mms-r12-server-ci.yml @@ -0,0 +1,61 @@ +name: MMS R1-R2 Server CI + +on: + pull_request: + paths: + - 'include/ariec61850/acse/**' + - 'include/ariec61850/mms/**' + - 'src/acse/**' + - 'src/mms/**' + - 'tools/mms_direct_control_server.cpp' + - 'tools/mms_urcb_server.cpp' + - 'tools/mms_direct_control_server/**' + - 'tests/test_mms_static_direct_control.cpp' + - 'tests/test_mms_server_urcb_report.cpp' + - 'scripts/build_mms_direct_control_server.ps1' + - '.github/workflows/mms-r12-server-ci.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + name: Linux ${{ matrix.compiler }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + compiler: [gcc, clang] + steps: + - uses: actions/checkout@v4 + - name: Select compiler + shell: bash + run: | + if [[ "${{ matrix.compiler }}" == "clang" ]]; then + echo "CC=clang" >> "$GITHUB_ENV" + echo "CXX=clang++" >> "$GITHUB_ENV" + else + echo "CC=gcc" >> "$GITHUB_ENV" + echo "CXX=g++" >> "$GITHUB_ENV" + fi + - name: Configure + run: cmake -S tools/mms_direct_control_server -B build-mms-r12 -DCMAKE_BUILD_TYPE=Release + - name: Build + run: cmake --build build-mms-r12 --parallel + - name: Test + run: ctest --test-dir build-mms-r12 --output-on-failure + - name: Help contracts + shell: bash + run: | + ./build-mms-r12/ariec61850_mms_direct_control_server --help + ./build-mms-r12/ariec61850_mms_urcb_server --help + + windows: + name: Windows MSVC + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - name: Configure, build, test and locate executables + shell: powershell + run: .\scripts\build_mms_direct_control_server.ps1 -BuildDir build-mms-r12 diff --git a/CMakeLists.txt b/CMakeLists.txt index 583c98f22..9d8f8f201 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,12 +67,46 @@ add_library(ariec61850_core add_library(ARIEC61850::core ALIAS ariec61850_core) +add_library(ariec61850_mms_server_core + src/asn1/ber_span_reader.cpp + src/osi/tpkt_span.cpp + src/osi/cotp_span.cpp + src/osi/session_span.cpp + src/osi/presentation_span.cpp + src/acse/association_span.cpp + src/mms/pdu_span.cpp + src/mms/services_span.cpp + src/mms/write_span.cpp + src/mms/data_set_span.cpp + src/mms/information_report_span.cpp + src/mms/buffered_selective_information_report_span.cpp + src/mms/static_object_table.cpp + src/mms/static_write.cpp + src/mms/static_data_set_table.cpp + src/mms/static_dispatcher.cpp + src/mms/connection_runtime.cpp + src/mms/static_server_session.cpp + src/mms/static_brcb_runtime.cpp + src/mms/static_brcb_control.cpp + src/mms/static_brcb_objects.cpp + src/mms/static_brcb_connection.cpp + src/mms/static_brcb_checkpoint_store.cpp + src/mms/static_brcb_state_codec.cpp) + +add_library(ARIEC61850::mms_server_core ALIAS ariec61850_mms_server_core) + target_compile_features(ariec61850_core PUBLIC cxx_std_20) target_include_directories(ariec61850_core PUBLIC $ $) +target_compile_features(ariec61850_mms_server_core PUBLIC cxx_std_20) +target_include_directories(ariec61850_mms_server_core + PUBLIC + $ + $) + if(WIN32) target_link_libraries(ariec61850_core PRIVATE ws2_32) endif() @@ -107,6 +141,8 @@ endfunction() ariec61850_apply_warnings(ariec61850_core) ariec61850_apply_sanitizers(ariec61850_core) +ariec61850_apply_warnings(ariec61850_mms_server_core) +ariec61850_apply_sanitizers(ariec61850_mms_server_core) if(ARIEC61850_BUILD_TOOLS) add_executable(ariec61850_pcap_interop_check tools/pcap_interop_check.cpp) @@ -156,6 +192,20 @@ if(ARIEC61850_BUILD_TOOLS) target_compile_features(ariec61850_file_transfer PRIVATE cxx_std_20) ariec61850_apply_warnings(ariec61850_file_transfer) ariec61850_apply_sanitizers(ariec61850_file_transfer) + + add_executable( + ariec61850_static_ied_server + tools/static_ied_server.cpp + tools/static_ied_server_sigpipe.cpp) + target_link_libraries( + ariec61850_static_ied_server + PRIVATE ARIEC61850::mms_server_core) + if(WIN32) + target_link_libraries(ariec61850_static_ied_server PRIVATE ws2_32) + endif() + target_compile_features(ariec61850_static_ied_server PRIVATE cxx_std_20) + ariec61850_apply_warnings(ariec61850_static_ied_server) + ariec61850_apply_sanitizers(ariec61850_static_ied_server) endif() if(ARIEC61850_BUILD_TESTS) @@ -337,6 +387,55 @@ if(ARIEC61850_BUILD_TESTS) ariec61850_apply_sanitizers(ariec61850_fuzz_smoke_tests) add_test(NAME ariec61850_fuzz_smoke_tests COMMAND ariec61850_fuzz_smoke_tests) + add_executable( + ariec61850_mms_server_session_tests + tests/test_mms_static_server_session.cpp) + target_link_libraries( + ariec61850_mms_server_session_tests + PRIVATE ARIEC61850::mms_server_core) + target_compile_features( + ariec61850_mms_server_session_tests PRIVATE cxx_std_20) + ariec61850_apply_warnings(ariec61850_mms_server_session_tests) + ariec61850_apply_sanitizers(ariec61850_mms_server_session_tests) + add_test( + NAME ariec61850_mms_server_session_tests + COMMAND ariec61850_mms_server_session_tests) + + add_executable( + ariec61850_mms_connection_runtime_tests + embedded/mms_connection_runtime_hard_profile_smoke.cpp) + target_link_libraries( + ariec61850_mms_connection_runtime_tests + PRIVATE ARIEC61850::mms_server_core) + target_compile_features( + ariec61850_mms_connection_runtime_tests PRIVATE cxx_std_20) + ariec61850_apply_warnings(ariec61850_mms_connection_runtime_tests) + ariec61850_apply_sanitizers(ariec61850_mms_connection_runtime_tests) + add_test( + NAME ariec61850_mms_connection_runtime_tests + COMMAND ariec61850_mms_connection_runtime_tests) + + foreach(brcb_smoke IN ITEMS + mms_brcb + mms_brcb_control + mms_brcb_connection + mms_brcb_objects + mms_brcb_lifecycle + mms_brcb_persistence + mms_brcb_recovery_v2 + mms_brcb_checkpoint_fallback + mms_negotiated_limits) + set(brcb_target "ariec61850_${brcb_smoke}_server_core_tests") + add_executable( + ${brcb_target} + ${CMAKE_CURRENT_SOURCE_DIR}/embedded/${brcb_smoke}_hard_profile_smoke.cpp) + target_link_libraries(${brcb_target} PRIVATE ARIEC61850::mms_server_core) + target_compile_features(${brcb_target} PRIVATE cxx_std_20) + ariec61850_apply_warnings(${brcb_target}) + ariec61850_apply_sanitizers(${brcb_target}) + add_test(NAME ${brcb_target} COMMAND ${brcb_target}) + endforeach() + if(ARIEC61850_BUILD_TOOLS) add_test( NAME ariec61850_pcap_interop_smoke @@ -357,6 +456,9 @@ if(ARIEC61850_BUILD_TESTS) add_test( NAME ariec61850_file_transfer_help COMMAND ariec61850_file_transfer --help) + add_test( + NAME ariec61850_static_ied_server_help + COMMAND ariec61850_static_ied_server --help) endif() endif() @@ -388,4 +490,4 @@ if(ARIEC61850_BUILD_FUZZERS) ariec61850_add_fuzzer(ariec61850_fuzz_mms_services fuzz/fuzz_mms_services.cpp) ariec61850_add_fuzzer(ariec61850_fuzz_mms_file_service fuzz/fuzz_mms_file_service.cpp) ariec61850_add_fuzzer(ariec61850_fuzz_reporting fuzz/fuzz_reporting.cpp) -endif() \ No newline at end of file +endif() diff --git a/docs/STATIC_IED_SERVER.md b/docs/STATIC_IED_SERVER.md new file mode 100644 index 000000000..8d7064324 --- /dev/null +++ b/docs/STATIC_IED_SERVER.md @@ -0,0 +1,80 @@ +# Static IEC 61850 server foundation + +`ariec61850_static_ied_server` is the first runnable desktop adapter for the +bounded MMS server core. It deliberately keeps platform networking outside the +protocol engine, matching the same boundary required by ESP-IDF/lwIP. + +```text +TCP listen/accept (WinSock or BSD sockets) + -> connected TcpByteStream callbacks + -> MmsStaticServerSession fixed-buffer pump + -> MmsStaticConnectionRuntime + -> MmsStaticApplicationDispatcher + -> static object/DataSet tables +``` + +The included desktop reference model is read-only: + +```text +ESP32S3IOLD0 +|- LLN0 Mod.stVal +|- LPHD1 PhyHealth.stVal +`- GGIO1 Ind1.stVal ... Ind8.stVal +``` + +## Desktop smoke run + +Build with `ARIEC61850_BUILD_TOOLS=ON`, then run an unprivileged development +endpoint such as port 8102: + +```powershell +.\ariec61850_static_ied_server.exe --port 8102 --digital-input-mask 0x35 --max-connections 1 +``` + +The executable default is TCP port **102**, the standard IEC 61850 MMS port. +Use `--port 8102` when local permissions or another process prevent binding +port 102. `--digital-input-mask` defaults to `0x35`; `--max-connections 0` +means unlimited sequential accepts. + +Implemented server services in this slice: + +- COTP connection accept with negotiated TPDU-size bounds; +- ACSE association accept and MMS Initiate response with the peer MMS PDU limit + retained and enforced on outbound confirmed responses; +- `GetNameList`; +- `GetVariableAccessAttributes`; +- `GetNamedVariableListAttributes` when a static DataSet table is supplied; +- `Read`; +- bounded `Write` only for object entries that explicitly provide a write + callback (the included desktop model provides none). + +The public `ARIEC61850::mms_server_core` also contains the bounded BRCB R1-R3 +runtime, ownership/lifecycle objects, retained replay/recovery, and negotiated +outbound-limit enforcement for InformationReport delivery. The current +`ariec61850_static_ied_server` reference model does **not** yet instantiate or +schedule those BRCB objects, so a desktop IEDScout browse/read pass must not be +reported as live BRCB interoperability. + +## ESP32/lwIP integration boundary + +The protocol core does not include an ESP-IDF header or own a listening socket. +An ESP-IDF application accepts a TCP/102 connection through lwIP, supplies +`TcpSendFn`/`TcpReceiveFn` callbacks, gives each connection its own +`MmsStaticConnectionRuntime`, `MmsStaticServerSession`, and fixed buffers, then +calls `poll_once()` from its task or event loop. Buffer capacities and the +maximum concurrent association count remain product-profile decisions. + +## Current limits + +- The desktop adapter serves connections sequentially. The core is per-session + and supports platform-owned multi-client orchestration, but the adapter does + not yet schedule multiple clients concurrently. +- Physical IEDScout browse/read acceptance is not yet recorded. +- The reference values are startup constants; a board adapter still needs to + bind the eight GGIO values to debounced input snapshots. +- The desktop reference adapter does not yet expose BRCB reporting or control + objects even though the server core contains those bounded primitives. +- Authentication, TLS, file services, runtime SCL model loading, and outbound + COTP segmentation are not implemented in this reference adapter. Responses + that cannot fit the negotiated peer TPDU/MMS limits fail closed instead of + emitting an oversized frame. \ No newline at end of file diff --git a/embedded/CMakeLists.txt b/embedded/CMakeLists.txt index 7eaba0dc5..5052569c3 100644 --- a/embedded/CMakeLists.txt +++ b/embedded/CMakeLists.txt @@ -34,6 +34,13 @@ set(ARIEC61850_EMBEDDED_SOURCES ${ARIEC61850_ROOT}/src/osi/presentation.cpp ${ARIEC61850_ROOT}/src/acse/association.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/osi/tpkt_span.cpp + ${ARIEC61850_ROOT}/src/osi/cotp_span.cpp + ${ARIEC61850_ROOT}/src/osi/session_span.cpp + ${ARIEC61850_ROOT}/src/osi/presentation_span.cpp + ${ARIEC61850_ROOT}/src/acse/association_span.cpp + ${ARIEC61850_ROOT}/src/mms/utc_time.cpp ${ARIEC61850_ROOT}/src/mms/data_value.cpp ${ARIEC61850_ROOT}/src/mms/data_codec.cpp @@ -41,7 +48,26 @@ set(ARIEC61850_EMBEDDED_SOURCES ${ARIEC61850_ROOT}/src/mms/services.cpp ${ARIEC61850_ROOT}/src/mms/invoke_router.cpp ${ARIEC61850_ROOT}/src/mms/association_runtime.cpp - ${ARIEC61850_ROOT}/src/mms/file_service.cpp) + ${ARIEC61850_ROOT}/src/mms/file_service.cpp + + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/write_span.cpp + ${ARIEC61850_ROOT}/src/mms/data_set_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_write.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_dispatcher.cpp + ${ARIEC61850_ROOT}/src/mms/connection_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_server_session.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_control.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_objects.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_connection.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_checkpoint_store.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_state_codec.cpp) add_library(ariec61850_embedded_core STATIC ${ARIEC61850_EMBEDDED_SOURCES}) add_library(ARIEC61850::embedded_core ALIAS ariec61850_embedded_core) @@ -98,6 +124,36 @@ ariec61850_apply_embedded_flags(ariec61850_embedded_link_smoke) enable_testing() add_test(NAME ariec61850_embedded_link_smoke COMMAND ariec61850_embedded_link_smoke) +foreach(server_smoke IN ITEMS + osi + session_presentation + association + mms_pdu + mms_get_name_list + mms_static_model + mms_write + mms_dispatcher + mms_connection_runtime + mms_negotiated_limits + mms_data_set + mms_brcb + mms_brcb_control + mms_brcb_connection + mms_brcb_objects + mms_brcb_lifecycle + mms_brcb_persistence + mms_brcb_recovery_v2) + set(server_smoke_target "ariec61850_embedded_${server_smoke}_smoke") + add_executable( + ${server_smoke_target} + ${CMAKE_CURRENT_LIST_DIR}/${server_smoke}_hard_profile_smoke.cpp) + target_link_libraries( + ${server_smoke_target} PRIVATE ARIEC61850::embedded_core) + target_compile_features(${server_smoke_target} PRIVATE cxx_std_20) + ariec61850_apply_embedded_flags(${server_smoke_target}) + add_test(NAME ${server_smoke_target} COMMAND ${server_smoke_target}) +endforeach() + message(STATUS "arstack61850 embedded profile: host services excluded") message(STATUS "arstack61850 embedded profile: RTTI disabled=${ARIEC61850_EMBEDDED_NO_RTTI}") message(STATUS "arstack61850 embedded profile: exceptions disabled=${ARIEC61850_EMBEDDED_NO_EXCEPTIONS}") diff --git a/embedded/association_hard_profile_smoke.cpp b/embedded/association_hard_profile_smoke.cpp new file mode 100644 index 000000000..cbb8d2328 --- /dev/null +++ b/embedded/association_hard_profile_smoke.cpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/acse/association_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kRequest{ + 0x0DU, 0xB6U, 0x05U, 0x06U, 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U, 0x14U, 0x02U, + 0x00U, 0x02U, 0x33U, 0x02U, 0x00U, 0x01U, 0x34U, 0x02U, 0x00U, 0x01U, 0xC1U, 0xA0U, + 0x31U, 0x81U, 0x9DU, 0xA0U, 0x03U, 0x80U, 0x01U, 0x01U, 0xA2U, 0x81U, 0x95U, 0x81U, + 0x04U, 0x00U, 0x00U, 0x00U, 0x01U, 0x82U, 0x04U, 0x00U, 0x00U, 0x00U, 0x01U, 0xA4U, + 0x23U, 0x30U, 0x0FU, 0x02U, 0x01U, 0x01U, 0x06U, 0x04U, 0x52U, 0x01U, 0x00U, 0x01U, + 0x30U, 0x04U, 0x06U, 0x02U, 0x51U, 0x01U, 0x30U, 0x10U, 0x02U, 0x01U, 0x03U, 0x06U, + 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, 0x01U, 0x30U, 0x04U, 0x06U, 0x02U, 0x51U, 0x01U, + 0x61U, 0x62U, 0x30U, 0x60U, 0x02U, 0x01U, 0x01U, 0xA0U, 0x5BU, 0x60U, 0x59U, 0xA1U, + 0x07U, 0x06U, 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, 0x03U, 0xA2U, 0x07U, 0x06U, 0x05U, + 0x29U, 0x01U, 0x87U, 0x67U, 0x01U, 0xA3U, 0x03U, 0x02U, 0x01U, 0x0CU, 0xA6U, 0x06U, + 0x06U, 0x04U, 0x29U, 0x01U, 0x87U, 0x67U, 0xA7U, 0x03U, 0x02U, 0x01U, 0x0CU, 0xBEU, + 0x33U, 0x28U, 0x31U, 0x06U, 0x02U, 0x51U, 0x01U, 0x02U, 0x01U, 0x03U, 0xA0U, 0x28U, + 0xA8U, 0x26U, 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, 0x81U, 0x01U, 0x0AU, 0x82U, 0x01U, + 0x0AU, 0x83U, 0x01U, 0x05U, 0xA4U, 0x16U, 0x80U, 0x01U, 0x01U, 0x81U, 0x03U, 0x05U, + 0xF1U, 0x00U, 0x82U, 0x0CU, 0x03U, 0xEEU, 0x1CU, 0x00U, 0x00U, 0x04U, 0x08U, 0x00U, + 0x00U, 0x79U, 0xEFU, 0x18U}; + +constexpr std::array kResponse{ + 0x0EU, 0x88U, 0x05U, 0x06U, 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U, 0x14U, 0x02U, + 0x00U, 0x02U, 0x33U, 0x02U, 0x00U, 0x01U, 0x34U, 0x02U, 0x00U, 0x01U, 0xC1U, 0x72U, + 0x31U, 0x70U, 0xA0U, 0x03U, 0x80U, 0x01U, 0x01U, 0xA2U, 0x69U, 0xA5U, 0x12U, 0x30U, + 0x07U, 0x80U, 0x01U, 0x00U, 0x81U, 0x02U, 0x51U, 0x01U, 0x30U, 0x07U, 0x80U, 0x01U, + 0x00U, 0x81U, 0x02U, 0x51U, 0x01U, 0x61U, 0x53U, 0x30U, 0x51U, 0x02U, 0x01U, 0x01U, + 0xA0U, 0x4CU, 0x61U, 0x4AU, 0xA1U, 0x07U, 0x06U, 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, + 0x03U, 0xA2U, 0x03U, 0x02U, 0x01U, 0x00U, 0xA3U, 0x05U, 0xA1U, 0x03U, 0x02U, 0x01U, + 0x00U, 0xBEU, 0x33U, 0x28U, 0x31U, 0x06U, 0x02U, 0x51U, 0x01U, 0x02U, 0x01U, 0x03U, + 0xA0U, 0x28U, 0xA9U, 0x26U, 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, 0x83U, 0x01U, 0x05U, 0xA4U, 0x16U, 0x80U, 0x01U, 0x01U, 0x81U, + 0x03U, 0x05U, 0xF1U, 0x00U, 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +constexpr std::array kMmsApplicationContext{ + 0x28U, 0xCAU, 0x22U, 0x02U, 0x03U}; +constexpr std::array kCalledApTitle{ + 0x29U, 0x01U, 0x87U, 0x67U, 0x01U}; +constexpr std::array kCallingApTitle{ + 0x29U, 0x01U, 0x87U, 0x67U}; +constexpr std::array kBerSyntax{0x51U, 0x01U}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +} // namespace + +int main() { + acse::AssociationRequestView request; + if (!acse::AcseSpanCodec::try_decode_association_request_view(kRequest, request)) { + return 1; + } + + if (request.session.kind != osi::SessionWireKind::connect || + request.presentation.mode_selector != 1U || + request.presentation.context_count != 2U || + request.acse_presentation_context_id != 1U || + request.mms_presentation_context_id != 3U || + !matches(request.aarq.application_context_name, kMmsApplicationContext) || + !matches(request.aarq.called_ap_title, kCalledApTitle) || + !request.aarq.called_ae_qualifier.has_value() || + *request.aarq.called_ae_qualifier != 12U || + !matches(request.aarq.calling_ap_title, kCallingApTitle) || + !request.aarq.calling_ae_qualifier.has_value() || + *request.aarq.calling_ae_qualifier != 12U || + !matches(request.aarq.user_information.direct_reference, kBerSyntax) || + request.aarq.user_information.indirect_reference != 3U || + !matches( + request.aarq.user_information.single_asn1_type, + std::array{ + 0xA8U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0xEEU, 0x1CU, 0x00U, 0x00U, 0x04U, + 0x08U, 0x00U, 0x00U, 0x79U, 0xEFU, 0x18U})) { + return 2; + } + + std::uint32_t acse_context{}; + std::uint32_t mms_context{}; + if (!request.presentation.try_context_id_for_abstract_syntax( + osi::PresentationSpanCodec::acse_abstract_syntax_name(), acse_context) || + !request.presentation.try_context_id_for_abstract_syntax( + osi::PresentationSpanCodec::mms_abstract_syntax_name(), mms_context) || + acse_context != 1U || mms_context != 3U) { + return 3; + } + + std::array response{}; + const auto required = acse::AcseSpanCodec::accept_response_size(request); + const auto result = acse::AcseSpanCodec::build_accept_response_into(request, response); + if (!required || *required != kResponse.size() || + !result.success() || result.bytes_written != kResponse.size() || + !matches( + std::span{response}.first(result.bytes_written), + kResponse)) { + return 4; + } + + std::array aare{}; + const auto aare_result = acse::AcseSpanCodec::encode_default_accept_aare_into(aare); + if (!aare_result.success() || + aare_result.bytes_written != acse::AcseSpanCodec::default_accept_aare_size() || + !matches( + acse::AcseSpanCodec::default_mms_initiate_response(), + std::array{ + 0xA9U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U})) { + return 5; + } + + for (std::uint32_t iteration = 0U; iteration < 10'000U; ++iteration) { + const auto repeated = acse::AcseSpanCodec::build_accept_response_into( + request, response); + if (!repeated.success() || repeated.bytes_written != kResponse.size() || + response[0] != osi::SessionSpanCodec::accept_code || + response[1] != 0x88U) { + return 6; + } + } + + constexpr std::array malformed{ + 0x0DU, 0x02U, 0xC1U, 0x00U}; + acse::AssociationRequestView rejected; + if (acse::AcseSpanCodec::try_decode_association_request_view(malformed, rejected)) { + return 7; + } + + return 0; +} \ No newline at end of file diff --git a/embedded/brcb_hard_profile/CMakeLists.txt b/embedded/brcb_hard_profile/CMakeLists.txt new file mode 100644 index 000000000..8945d93b4 --- /dev/null +++ b/embedded/brcb_hard_profile/CMakeLists.txt @@ -0,0 +1,200 @@ +cmake_minimum_required(VERSION 3.20) + +project(ARIEC61850BrcbHardProfile + VERSION 0.3.0 + DESCRIPTION "No-exception bounded IEC 61850 buffered-report profile" + LANGUAGES CXX) + +set(ARIEC61850_ROOT "${CMAKE_CURRENT_LIST_DIR}/../..") + +if(MSVC) + message(FATAL_ERROR "BRCB hard profile is validated with GCC/Clang -fno-exceptions.") +endif() + +function(ariec61850_configure_brcb_hard_target target) + target_compile_features(${target} PRIVATE cxx_std_20) + target_include_directories(${target} PRIVATE ${ARIEC61850_ROOT}/include) + target_compile_definitions(${target} PRIVATE + ARIEC61850_EMBEDDED_PROFILE=1 + ARIEC61850_HOST_SERVICES=0 + ARIEC61850_NO_EXCEPTIONS=1) + target_compile_options(${target} PRIVATE + -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Werror + -fno-exceptions -fno-rtti) +endfunction() + +add_executable(ariec61850_hard_mms_brcb_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_smoke) + +add_executable(ariec61850_hard_mms_brcb_control_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_control_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_control.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_control_smoke) + +add_executable(ariec61850_hard_mms_brcb_objects_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_objects_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/write_span.cpp + ${ARIEC61850_ROOT}/src/mms/data_set_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_dispatcher.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_control.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_objects.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_objects_smoke) + +add_executable(ariec61850_hard_mms_brcb_lifecycle_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_lifecycle_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/osi/tpkt_span.cpp + ${ARIEC61850_ROOT}/src/osi/cotp_span.cpp + ${ARIEC61850_ROOT}/src/osi/session_span.cpp + ${ARIEC61850_ROOT}/src/osi/presentation_span.cpp + ${ARIEC61850_ROOT}/src/acse/association_span.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/write_span.cpp + ${ARIEC61850_ROOT}/src/mms/data_set_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_write.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_dispatcher.cpp + ${ARIEC61850_ROOT}/src/mms/connection_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_control.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_lifecycle_smoke) + +add_executable(ariec61850_hard_mms_brcb_connection_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_connection_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/osi/tpkt_span.cpp + ${ARIEC61850_ROOT}/src/osi/cotp_span.cpp + ${ARIEC61850_ROOT}/src/osi/session_span.cpp + ${ARIEC61850_ROOT}/src/osi/presentation_span.cpp + ${ARIEC61850_ROOT}/src/acse/association_span.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/write_span.cpp + ${ARIEC61850_ROOT}/src/mms/data_set_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_write.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_dispatcher.cpp + ${ARIEC61850_ROOT}/src/mms/connection_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_control.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_connection.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_connection_smoke) + +add_executable(ariec61850_hard_mms_brcb_recovery_v2_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_recovery_v2_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_state_codec.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_recovery_v2_smoke) + +add_executable(ariec61850_hard_mms_brcb_persistence_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_persistence_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_state_codec.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_checkpoint_store.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_persistence_smoke) + +add_executable(ariec61850_hard_mms_brcb_checkpoint_fallback_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_brcb_checkpoint_fallback_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_state_codec.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_checkpoint_store.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_brcb_checkpoint_fallback_smoke) + +add_executable(ariec61850_hard_mms_negotiated_limits_smoke + ${CMAKE_CURRENT_LIST_DIR}/../mms_negotiated_limits_hard_profile_smoke.cpp + ${ARIEC61850_ROOT}/src/asn1/ber_span_reader.cpp + ${ARIEC61850_ROOT}/src/osi/tpkt_span.cpp + ${ARIEC61850_ROOT}/src/osi/cotp_span.cpp + ${ARIEC61850_ROOT}/src/osi/session_span.cpp + ${ARIEC61850_ROOT}/src/osi/presentation_span.cpp + ${ARIEC61850_ROOT}/src/acse/association_span.cpp + ${ARIEC61850_ROOT}/src/mms/pdu_span.cpp + ${ARIEC61850_ROOT}/src/mms/services_span.cpp + ${ARIEC61850_ROOT}/src/mms/write_span.cpp + ${ARIEC61850_ROOT}/src/mms/data_set_span.cpp + ${ARIEC61850_ROOT}/src/mms/information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_object_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_write.cpp + ${ARIEC61850_ROOT}/src/mms/static_data_set_table.cpp + ${ARIEC61850_ROOT}/src/mms/static_dispatcher.cpp + ${ARIEC61850_ROOT}/src/mms/connection_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/buffered_selective_information_report_span.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_runtime.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_control.cpp + ${ARIEC61850_ROOT}/src/mms/static_brcb_connection.cpp) +ariec61850_configure_brcb_hard_target(ariec61850_hard_mms_negotiated_limits_smoke) + +enable_testing() +add_test(NAME ariec61850_hard_mms_brcb_smoke COMMAND ariec61850_hard_mms_brcb_smoke) +add_test(NAME ariec61850_hard_mms_brcb_control_smoke COMMAND ariec61850_hard_mms_brcb_control_smoke) +add_test(NAME ariec61850_hard_mms_brcb_objects_smoke COMMAND ariec61850_hard_mms_brcb_objects_smoke) +add_test(NAME ariec61850_hard_mms_brcb_lifecycle_smoke COMMAND ariec61850_hard_mms_brcb_lifecycle_smoke) +add_test(NAME ariec61850_hard_mms_brcb_connection_smoke COMMAND ariec61850_hard_mms_brcb_connection_smoke) +add_test(NAME ariec61850_hard_mms_brcb_recovery_v2_smoke COMMAND ariec61850_hard_mms_brcb_recovery_v2_smoke) +add_test(NAME ariec61850_hard_mms_brcb_persistence_smoke COMMAND ariec61850_hard_mms_brcb_persistence_smoke) +add_test(NAME ariec61850_hard_mms_brcb_checkpoint_fallback_smoke COMMAND ariec61850_hard_mms_brcb_checkpoint_fallback_smoke) +add_test(NAME ariec61850_hard_mms_negotiated_limits_smoke COMMAND ariec61850_hard_mms_negotiated_limits_smoke) + +message(STATUS "arstack61850 BRCB hard profile: bounded RAM queue + EntryID + BufOvfl=ON") +message(STATUS "arstack61850 BRCB hard profile: association ownership + ResvTms gate=ON") +message(STATUS "arstack61850 BRCB hard profile: MMS RptEna/EntryID/PurgeBuf/ResvTms/Owner=ON") +message(STATUS "arstack61850 BRCB hard profile: transport association lifecycle bridge=ON") +message(STATUS "arstack61850 BRCB hard profile: retained-history recovery state v2 + v1 restore=ON") +message(STATUS "arstack61850 BRCB hard profile: retry-safe two-phase owner-bound delivery=ON") +message(STATUS "arstack61850 BRCB hard profile: negotiated COTP/MMS outbound bounds=ON") +message(STATUS "arstack61850 BRCB hard profile: dual-bank checkpoint + one-bank recovery=ON") +message(STATUS "arstack61850 BRCB hard profile: platform non-volatile adapter=NOT_CLAIMED") +message(STATUS "arstack61850 BRCB hard profile: exceptions/RTTI disabled=ON") \ No newline at end of file diff --git a/embedded/mms_brcb_checkpoint_fallback_hard_profile_smoke.cpp b/embedded/mms_brcb_checkpoint_fallback_hard_profile_smoke.cpp new file mode 100644 index 000000000..4741e6002 --- /dev/null +++ b/embedded/mms_brcb_checkpoint_fallback_hard_profile_smoke.cpp @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_checkpoint_store.hpp" +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::size_t kBankBytes = 4096U; +constexpr std::size_t kStorageBytes = kBankBytes * 2U; + +bool gValue = true; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +const std::array kObjects{ + mms::MmsStaticObjectEntry{"LD0", "X1", kBooleanType, read_boolean, &gValue, false}}; +const mms::MmsStaticObjectTable kObjectTable{kObjects}; +const std::array kMembers{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; +const std::array kDataSets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", kMembers, false}}; +const mms::MmsStaticDataSetTable kDataSetTable{kDataSets}; +const mms::MmsStaticBrcbDefinition kDefinition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Events", + "LD0", + "LLN0$Events", + 17U, + {0x7FU, 0x80U}, + 20U, + 0x70U}; + +struct RuntimeFixture final { + std::array slot0{}; + std::array slot1{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}, + mms::MmsStaticBrcbSlot{slot1}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime runtime{ + kDefinition, pending, slots, kObjectTable, kDataSetTable}; +}; + +struct FakeStorage final { + std::array bytes{}; + bool fail_bank0_reads{}; + bool fail_bank1_reads{}; + + FakeStorage() noexcept { + std::fill(bytes.begin(), bytes.end(), std::uint8_t{0xFFU}); + } +}; + +[[nodiscard]] bool storage_read( + void* context, + const std::size_t offset, + const std::span destination) noexcept { + auto* storage = static_cast(context); + if (storage == nullptr || offset > storage->bytes.size() || + destination.size() > storage->bytes.size() - offset) { + return false; + } + if ((storage->fail_bank0_reads && offset < kBankBytes) || + (storage->fail_bank1_reads && offset >= kBankBytes)) { + return false; + } + std::copy_n( + storage->bytes.begin() + static_cast(offset), + destination.size(), + destination.begin()); + return true; +} + +[[nodiscard]] bool storage_write( + void* context, + const std::size_t offset, + const std::span source) noexcept { + auto* storage = static_cast(context); + if (storage == nullptr || offset > storage->bytes.size() || + source.size() > storage->bytes.size() - offset) { + return false; + } + std::copy( + source.begin(), + source.end(), + storage->bytes.begin() + static_cast(offset)); + return true; +} + +[[nodiscard]] bool storage_erase( + void* context, + const std::size_t offset, + const std::size_t bytes) noexcept { + auto* storage = static_cast(context); + if (storage == nullptr || offset > storage->bytes.size() || + bytes > storage->bytes.size() - offset) { + return false; + } + std::fill_n( + storage->bytes.begin() + static_cast(offset), + bytes, + std::uint8_t{0xFFU}); + return true; +} + +[[nodiscard]] bool storage_sync(void* context) noexcept { + return context != nullptr; +} + +} // namespace + +int main() { + FakeStorage storage; + const mms::MmsStaticBrcbStorageBackend backend{ + &storage, + storage.bytes.size(), + storage_read, + storage_write, + storage_erase, + storage_sync}; + const mms::MmsStaticBrcbCheckpointStore store{backend, kBankBytes}; + if (!store.valid()) { + return 1; + } + + RuntimeFixture source; + if (!source.runtime.initialize()) { + return 2; + } + std::array state_buffer{}; + const auto first = store.checkpoint(source.runtime, state_buffer); + const auto second = store.checkpoint(source.runtime, state_buffer); + if (!first.success() || first.generation != 1U || first.bank_index != 0U || + !second.success() || second.generation != 2U || second.bank_index != 1U) { + return 3; + } + + // Bank 0 is unreadable, but the newer committed bank 1 remains valid. + storage.fail_bank0_reads = true; + RuntimeFixture from_bank1; + if (!from_bank1.runtime.initialize()) { + return 4; + } + const auto bank1_restore = store.restore(from_bank1.runtime, state_buffer); + if (!bank1_restore.success() || bank1_restore.generation != 2U || + bank1_restore.bank_index != 1U) { + return 5; + } + + // Reverse the fault. The older bank 0 must remain a usable recovery point. + storage.fail_bank0_reads = false; + storage.fail_bank1_reads = true; + RuntimeFixture from_bank0; + if (!from_bank0.runtime.initialize()) { + return 6; + } + const auto bank0_restore = store.restore(from_bank0.runtime, state_buffer); + if (!bank0_restore.success() || bank0_restore.generation != 1U || + bank0_restore.bank_index != 0U) { + return 7; + } + + return 0; +} diff --git a/embedded/mms_brcb_connection_hard_profile_smoke.cpp b/embedded/mms_brcb_connection_hard_profile_smoke.cpp new file mode 100644 index 000000000..606f84232 --- /dev/null +++ b/embedded/mms_brcb_connection_hard_profile_smoke.cpp @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/mms/static_brcb_connection.hpp" +#include "ariec61850/mms/static_dispatcher.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kReportTime{ + 0x00U, 0x00U, 0x12U, 0x34U, 0x00U, 0x01U}; +constexpr std::array kOwnerA{0xAAU, 0x01U}; +constexpr std::array kOwnerB{0xBBU, 0x01U}; + +constexpr std::array kAssociationRequest{ + 0x0DU,0xB6U,0x05U,0x06U,0x13U,0x01U,0x00U,0x16U,0x01U,0x02U,0x14U,0x02U, + 0x00U,0x02U,0x33U,0x02U,0x00U,0x01U,0x34U,0x02U,0x00U,0x01U,0xC1U,0xA0U, + 0x31U,0x81U,0x9DU,0xA0U,0x03U,0x80U,0x01U,0x01U,0xA2U,0x81U,0x95U,0x81U, + 0x04U,0x00U,0x00U,0x00U,0x01U,0x82U,0x04U,0x00U,0x00U,0x00U,0x01U,0xA4U, + 0x23U,0x30U,0x0FU,0x02U,0x01U,0x01U,0x06U,0x04U,0x52U,0x01U,0x00U,0x01U, + 0x30U,0x04U,0x06U,0x02U,0x51U,0x01U,0x30U,0x10U,0x02U,0x01U,0x03U,0x06U, + 0x05U,0x28U,0xCAU,0x22U,0x02U,0x01U,0x30U,0x04U,0x06U,0x02U,0x51U,0x01U, + 0x61U,0x62U,0x30U,0x60U,0x02U,0x01U,0x01U,0xA0U,0x5BU,0x60U,0x59U,0xA1U, + 0x07U,0x06U,0x05U,0x28U,0xCAU,0x22U,0x02U,0x03U,0xA2U,0x07U,0x06U,0x05U, + 0x29U,0x01U,0x87U,0x67U,0x01U,0xA3U,0x03U,0x02U,0x01U,0x0CU,0xA6U,0x06U, + 0x06U,0x04U,0x29U,0x01U,0x87U,0x67U,0xA7U,0x03U,0x02U,0x01U,0x0CU,0xBEU, + 0x33U,0x28U,0x31U,0x06U,0x02U,0x51U,0x01U,0x02U,0x01U,0x03U,0xA0U,0x28U, + 0xA8U,0x26U,0x80U,0x03U,0x00U,0xFDU,0xE8U,0x81U,0x01U,0x0AU,0x82U,0x01U, + 0x0AU,0x83U,0x01U,0x05U,0xA4U,0x16U,0x80U,0x01U,0x01U,0x81U,0x03U,0x05U, + 0xF1U,0x00U,0x82U,0x0CU,0x03U,0xEEU,0x1CU,0x00U,0x00U,0x04U,0x08U,0x00U, + 0x00U,0x79U,0xEFU,0x18U}; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) return {wire::EncodeStatus::value_out_of_range, 0U, required}; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] mms::MmsStaticBrcbClientIdentity identity( + const std::uint64_t association, + const std::array& owner) noexcept { + mms::MmsStaticBrcbClientIdentity result; + result.association_id = association; + result.owner[0] = owner[0]; + result.owner[1] = owner[1]; + result.owner_size = owner.size(); + return result; +} + +[[nodiscard]] mms::MmsStaticConnectionPolicy connection_policy( + const std::uint64_t association, + const std::array& owner) noexcept { + mms::MmsStaticConnectionPolicy result; + result.association_id = association; + result.owner[0] = owner[0]; + result.owner[1] = owner[1]; + result.owner_size = owner.size(); + return result; +} + +[[nodiscard]] bool establish( + mms::MmsStaticConnectionRuntime& connection, + std::span request, + std::span response, + std::span workspace, + std::span scratch) noexcept { + constexpr std::array tpdu_size{0x0AU}; + constexpr std::array source_tsap{0x00U, 0x01U}; + constexpr std::array destination_tsap{0x00U, 0x01U}; + const std::array parameters{ + osi::CotpParameterView{osi::CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + osi::CotpParameterView{osi::CotpSpanCodec::source_tsap_parameter, source_tsap}, + osi::CotpParameterView{osi::CotpSpanCodec::destination_tsap_parameter, destination_tsap}}; + + const auto cr = osi::CotpSpanCodec::encode_connection_request_into( + 0x0001U, parameters, scratch); + if (!cr.success()) return false; + const auto cr_tpkt = osi::TpktSpanCodec::encode_into( + scratch.first(cr.bytes_written), request); + if (!cr_tpkt.success()) return false; + auto result = connection.process_tcp_window( + request.first(cr_tpkt.bytes_written), response, workspace); + if (!result.response_ready() || + connection.state() != mms::MmsStaticConnectionState::awaiting_association) { + return false; + } + + const auto cotp = osi::CotpSpanCodec::encode_data_into(kAssociationRequest, scratch); + if (!cotp.success()) return false; + const auto association = osi::TpktSpanCodec::encode_into( + scratch.first(cotp.bytes_written), request); + if (!association.success()) return false; + result = connection.process_tcp_window( + request.first(association.bytes_written), response, workspace); + return result.response_ready() && + connection.state() == mms::MmsStaticConnectionState::established && + connection.mms_presentation_context_id() == 3U; +} + +[[nodiscard]] bool decode_report_frame( + const std::span frame, + mms::MmsInformationReportView& report) noexcept { + osi::TpktFrameView tpkt; + osi::CotpTpduView cotp; + osi::SessionDataTransferView session; + osi::PresentationPdvView pdv; + return osi::TpktSpanCodec::try_decode_view(frame, tpkt) && + osi::CotpSpanCodec::try_decode_view(tpkt.payload, cotp) && + cotp.kind == osi::CotpWireKind::data && cotp.end_of_transmission && + osi::SessionSpanCodec::try_decode_data_transfer_view(cotp.user_data, session) && + osi::PresentationSpanCodec::try_decode_fully_encoded_data_view( + session.presentation_payload, pdv) && + pdv.context_id == 3U && + mms::MmsInformationReportSpanCodec::try_decode_information_report( + pdv.single_asn1_type, report); +} + +[[nodiscard]] bool decode_boolean( + const mms::MmsReadAccessResultView& item, + bool& value) noexcept { + value = false; + if (!item.success) return false; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(item.encoded_data, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 3 || tlv.constructed || tlv.value.size() != 1U) return false; + value = tlv.value[0] != 0U; + return true; +} + +} // namespace + +int main() { + bool value = true; + const std::array objects{ + mms::MmsStaticObjectEntry{"LD0", "X1", kBooleanType, read_boolean, &value, false}}; + const mms::MmsStaticObjectTable object_table{objects}; + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + const mms::MmsStaticApplicationDispatcher dispatcher{object_table, data_set_table}; + + const auto owner_a1 = identity(101U, kOwnerA); + const auto owner_a2 = identity(102U, kOwnerA); + const auto foreign_b = identity(201U, kOwnerB); + mms::MmsStaticConnectionRuntime connection{ + dispatcher, connection_policy(owner_a1.association_id, kOwnerA)}; + mms::MmsStaticConnectionRuntime foreign_connection{ + dispatcher, connection_policy(foreign_b.association_id, kOwnerB)}; + + const mms::MmsStaticBrcbDefinition definition{ + "LD0", "LLN0$BR$Events", "LD0/LLN0$BR$Events", + "LD0", "LLN0$Events", 4U, {0x7FU, 0x80U}, 0U, 0x70U}; + std::array slot_storage{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot_storage}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime reports{ + definition, pending, slots, object_table, data_set_table}; + if (!reports.initialize()) { + return 1; + } + mms::MmsStaticBrcbControl control{reports}; + if (control.reserve(owner_a1, 5U, 100U) != mms::MmsStaticBrcbControlStatus::ok || + control.set_report_enabled(owner_a1, true, 100U) != + mms::MmsStaticBrcbControlStatus::ok || + reports.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 100U) != + mms::MmsStaticBrcbStatus::ok) { + return 2; + } + + mms::MmsStaticBrcbCapturePlan plan; + std::array staging{}; + std::array value_workspace{}; + if (!reports.next_due(100U, plan) || + !reports.capture(plan, kReportTime, staging, value_workspace).success() || + reports.queue_size() != 1U) { + return 3; + } + + value = false; + std::array request{}; + std::array response{}; + std::array workspace{}; + std::array scratch{}; + auto delivery = mms::MmsStaticBrcbConnection::poll( + connection, control, reports, 100U, response, workspace); + if (delivery.status != mms::MmsStaticBrcbConnectionStatus::not_established || + reports.queue_size() != 1U) { + return 4; + } + if (!establish(connection, request, response, workspace, scratch) || + !establish(foreign_connection, request, response, workspace, scratch)) { + return 5; + } + + // A foreign established association must not see or consume the owner's entry. + delivery = mms::MmsStaticBrcbConnection::poll( + foreign_connection, control, reports, 100U, response, workspace); + if (delivery.status != mms::MmsStaticBrcbConnectionStatus::access_denied || + reports.queue_size() != 1U) { + return 6; + } + + mms::MmsStaticBrcbEntryView held_entry; + if (!reports.front(held_entry) || held_entry.entry_id.size() != 8U || + held_entry.entry_id[7U] != 1U || + control.set_report_enabled(owner_a1, false, 100U) != + mms::MmsStaticBrcbControlStatus::ok) { + return 7; + } + delivery = mms::MmsStaticBrcbConnection::poll( + connection, control, reports, 100U, response, workspace); + if (delivery.status != mms::MmsStaticBrcbConnectionStatus::reporting_disabled || + reports.queue_size() != 1U || !reports.front(held_entry) || + held_entry.entry_id[7U] != 1U || + control.set_report_enabled(owner_a1, true, 100U) != + mms::MmsStaticBrcbControlStatus::ok) { + return 8; + } + + std::array tiny_response{}; + delivery = mms::MmsStaticBrcbConnection::poll( + connection, control, reports, 100U, tiny_response, workspace); + if (delivery.status != mms::MmsStaticBrcbConnectionStatus::response_buffer_too_small || + delivery.required_response_bytes <= tiny_response.size() || + delivery.entry_id[7U] != 1U || reports.queue_size() != 1U) { + return 9; + } + + std::array tiny_workspace{}; + delivery = mms::MmsStaticBrcbConnection::poll( + connection, control, reports, 100U, response, tiny_workspace); + if (delivery.status != mms::MmsStaticBrcbConnectionStatus::workspace_too_small || + delivery.required_workspace_bytes <= tiny_workspace.size() || + delivery.entry_id[7U] != 1U || reports.queue_size() != 1U) { + return 10; + } + + // Staging is retry-safe: no delivery cursor movement until commit_sent(). + delivery = mms::MmsStaticBrcbConnection::poll( + connection, control, reports, 100U, response, workspace); + if (!delivery.response_ready() || delivery.bytes_written == 0U || + delivery.entry_id[7U] != 1U || delivery.sequence_number != 1U || + delivery.buffer_overflow || reports.queue_size() != 1U) { + return 11; + } + const auto retry = mms::MmsStaticBrcbConnection::poll( + connection, control, reports, 100U, response, workspace); + if (!retry.response_ready() || retry.entry_id != delivery.entry_id || + reports.queue_size() != 1U) { + return 12; + } + + mms::MmsInformationReportView decoded; + if (!decode_report_frame( + std::span{response}.first(retry.bytes_written), decoded) || + decoded.item_count != 12U) { + return 13; + } + mms::MmsReadAccessResultView item; + bool historical_value = false; + if (!decoded.try_item(10U, item) || !decode_boolean(item, historical_value) || + !historical_value) { + return 14; + } + + // A staged token is not transferable: a foreign association cannot commit + // an EntryID staged by the legitimate BRCB owner. + if (mms::MmsStaticBrcbConnection::commit_sent( + foreign_connection, control, reports, 100U, retry) || + reports.queue_size() != 1U) { + return 15; + } + if (!mms::MmsStaticBrcbConnection::commit_sent( + connection, control, reports, 100U, retry) || + reports.queue_size() != 0U) { + return 16; + } + + // Queue another historical entry, close A1, then reclaim with the same stable + // Owner on association A2. The retained entry must remain retryable. + if (reports.notify(0U, mms::MmsStaticBrcbEventReason::data_update, 200U) != + mms::MmsStaticBrcbStatus::ok || + !reports.next_due(200U, plan) || + !reports.capture(plan, kReportTime, staging, value_workspace).success() || + reports.queue_size() != 1U) { + return 17; + } + connection.reset(); + control.on_association_closed(owner_a1.association_id, 200U); + delivery = mms::MmsStaticBrcbConnection::poll( + connection, control, reports, 200U, response, workspace); + if (delivery.status != mms::MmsStaticBrcbConnectionStatus::not_established || + reports.queue_size() != 1U || + control.reserve(owner_a2, 5U, 201U) != mms::MmsStaticBrcbControlStatus::ok || + control.set_report_enabled(owner_a2, true, 201U) != + mms::MmsStaticBrcbControlStatus::ok) { + return 18; + } + + mms::MmsStaticConnectionRuntime reconnect{ + dispatcher, connection_policy(owner_a2.association_id, kOwnerA)}; + if (!establish(reconnect, request, response, workspace, scratch)) { + return 19; + } + delivery = mms::MmsStaticBrcbConnection::poll( + reconnect, control, reports, 201U, response, workspace); + if (!delivery.response_ready() || delivery.entry_id[7U] != 2U || + reports.queue_size() != 1U || + !mms::MmsStaticBrcbConnection::commit_sent( + reconnect, control, reports, 201U, delivery) || + reports.queue_size() != 0U || + mms::MmsStaticBrcbConnection::poll( + reconnect, control, reports, 201U, response, workspace).status != + mms::MmsStaticBrcbConnectionStatus::no_report_available) { + return 20; + } + + return 0; +} diff --git a/embedded/mms_brcb_control_hard_profile_smoke.cpp b/embedded/mms_brcb_control_hard_profile_smoke.cpp new file mode 100644 index 000000000..63c480c21 --- /dev/null +++ b/embedded/mms_brcb_control_hard_profile_smoke.cpp @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_control.hpp" + +#include +#include +#include +#include + +namespace { +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] mms::MmsStaticBrcbClientIdentity client( + const std::uint64_t association, + const std::uint8_t first, + const std::uint8_t second) noexcept { + mms::MmsStaticBrcbClientIdentity identity; + identity.association_id = association; + identity.owner[0] = first; + identity.owner[1] = second; + identity.owner_size = 2U; + return identity; +} + +} // namespace + +int main() { + bool value = true; + const std::array objects{ + mms::MmsStaticObjectEntry{"LD0", "X1", kBooleanType, read_boolean, &value, false}}; + const mms::MmsStaticObjectTable object_table{objects}; + + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + + const mms::MmsStaticBrcbDefinition definition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Events", + "LD0", + "LLN0$Events", + 1U, + {0x7FU, 0x80U}, + 20U, + 0x70U}; + + std::array slot0{}; + std::array slot1{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}, + mms::MmsStaticBrcbSlot{slot1}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime reports{ + definition, pending, slots, object_table, data_set_table}; + if (!reports.initialize()) { + return 1; + } + + mms::MmsStaticBrcbControl control{reports}; + const auto a1 = client(101U, 0xAAU, 0x01U); + const auto a2 = client(102U, 0xAAU, 0x01U); + const auto a3 = client(103U, 0xAAU, 0x01U); + const auto b1 = client(201U, 0xBBU, 0x01U); + const std::array missing_entry{0U,0U,0U,0U,0U,0U,0U,1U}; + + if (control.reserve(a1, 5U, 100U) != mms::MmsStaticBrcbControlStatus::ok) { + return 2; + } + auto state = control.state(100U); + if (!state.reserved || !state.owner_connected || state.report_enabled || + state.association_id != 101U || state.resv_tms_seconds != 5U || + state.owner.size() != 2U || state.owner[0] != 0xAAU) { + return 3; + } + + // A second live association, even with the same stable Owner identity, + // cannot steal a currently connected reservation. A different Owner also + // cannot reserve or enable the BRCB. + if (control.reserve(a2, 5U, 200U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.reserve(b1, 5U, 200U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.set_report_enabled(b1, true, 200U) != + mms::MmsStaticBrcbControlStatus::object_access_denied) { + return 4; + } + + if (control.set_report_enabled(a1, true, 300U) != + mms::MmsStaticBrcbControlStatus::ok || + !reports.enabled() || + control.replay_from(a1, missing_entry, 300U) != + mms::MmsStaticBrcbControlStatus::temporarily_unavailable) { + return 5; + } + + // Association loss always disables reporting. Non-zero ResvTms retains the + // stable Owner identity so the same Owner can reclaim before timeout. + control.on_association_closed(a1.association_id, 1'000U); + state = control.state(1'000U); + if (reports.enabled() || !state.reserved || state.owner_connected || + state.association_id != 0U || state.reservation_expires_at_ms != 6'000U || + control.reserve(b1, 5U, 2'000U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.reserve(a2, 7U, 2'500U) != mms::MmsStaticBrcbControlStatus::ok) { + return 6; + } + + // The old association token is dead; only the reconnecting live association + // may control replay/purge/release. + if (control.set_report_enabled(a1, true, 2'600U) != + mms::MmsStaticBrcbControlStatus::object_access_denied || + control.replay_from(a2, missing_entry, 2'600U) != + mms::MmsStaticBrcbControlStatus::entry_not_found || + control.purge_buffer(a2, 2'600U) != mms::MmsStaticBrcbControlStatus::ok || + control.release(a2, 2'600U) != mms::MmsStaticBrcbControlStatus::ok || + control.state(2'600U).reserved) { + return 7; + } + + // ResvTms==0 releases immediately on association loss. + if (control.reserve(a3, 0U, 3'000U) != mms::MmsStaticBrcbControlStatus::ok || + control.set_report_enabled(a3, true, 3'000U) != mms::MmsStaticBrcbControlStatus::ok) { + return 8; + } + control.on_association_closed(a3.association_id, 3'100U); + if (reports.enabled() || control.state(3'100U).reserved) { + return 9; + } + + // A retained reservation blocks another Owner until the exact expiry, then + // automatically becomes available without dynamic allocation or threads. + if (control.reserve(a1, 2U, 10'000U) != mms::MmsStaticBrcbControlStatus::ok) { + return 10; + } + control.on_association_closed(a1.association_id, 10'000U); + if (control.reserve(b1, 1U, 11'999U) != + mms::MmsStaticBrcbControlStatus::object_access_denied) { + return 11; + } + control.tick(12'000U); + if (control.state(12'000U).reserved || + control.reserve(b1, 1U, 12'000U) != mms::MmsStaticBrcbControlStatus::ok) { + return 12; + } + + return 0; +} diff --git a/embedded/mms_brcb_hard_profile_smoke.cpp b/embedded/mms_brcb_hard_profile_smoke.cpp new file mode 100644 index 000000000..60d4d066a --- /dev/null +++ b/embedded/mms_brcb_hard_profile_smoke.cpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/mms/information_report_span.hpp" +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kReportTime{ + 0x00U, 0x00U, 0x12U, 0x34U, 0x00U, 0x01U}; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] bool decode_boolean( + const mms::MmsReadAccessResultView& item, + bool& value) noexcept { + value = false; + if (!item.success) return false; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(item.encoded_data, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 3 || tlv.constructed || tlv.value.size() != 1U) { + return false; + } + value = tlv.value[0] != 0U; + return true; +} + +[[nodiscard]] bool decode_bit_string( + const mms::MmsReadAccessResultView& item, + const std::uint8_t unused, + const std::uint8_t value) noexcept { + if (!item.success) return false; + asn1::BerTlvView tlv; + return asn1::BerSpanReader::try_read_exact(item.encoded_data, tlv) && + tlv.tag_class == asn1::BerClass::context_specific && + tlv.tag_number == 4 && !tlv.constructed && tlv.value.size() == 2U && + tlv.value[0] == unused && tlv.value[1] == value; +} + +[[nodiscard]] bool decode_octet_string( + const mms::MmsReadAccessResultView& item, + const std::span expected) noexcept { + if (!item.success) return false; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(item.encoded_data, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 9 || tlv.constructed || tlv.value.size() != expected.size()) { + return false; + } + for (std::size_t index = 0U; index < expected.size(); ++index) { + if (tlv.value[index] != expected[index]) return false; + } + return true; +} + +[[nodiscard]] bool expected_entry( + const std::array& entry, + const std::uint64_t value) noexcept { + for (std::size_t index = 0U; index < entry.size(); ++index) { + const auto shift = static_cast((entry.size() - 1U - index) * 8U); + if (entry[index] != static_cast((value >> shift) & 0xFFU)) { + return false; + } + } + return true; +} + +} // namespace + +int main() { + bool first = true; + bool second = false; + bool third = true; + const std::array objects{ + mms::MmsStaticObjectEntry{"LD0", "X1", kBooleanType, read_boolean, &first, false}, + mms::MmsStaticObjectEntry{"LD0", "X2", kBooleanType, read_boolean, &second, false}, + mms::MmsStaticObjectEntry{"LD0", "X3", kBooleanType, read_boolean, &third, false}}; + const mms::MmsStaticObjectTable object_table{objects}; + + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "X1"}, + mms::MmsStaticDataSetMember{"LD0", "X2"}, + mms::MmsStaticDataSetMember{"LD0", "X3"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + + const mms::MmsStaticBrcbDefinition definition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Events", + "LD0", + "LLN0$Events", + 11U, + {0x7FU, 0x80U}, + 20U, + 0x70U}; + + std::array slot0{}; + std::array slot1{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}, + mms::MmsStaticBrcbSlot{slot1}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime runtime{ + definition, pending, slots, object_table, data_set_table}; + if (!runtime.initialize() || !runtime.valid() || runtime.enabled() || + runtime.queue_size() != 0U || runtime.retained_size() != 0U || + runtime.queue_capacity() != 2U || runtime.replay_gap()) { + return 1; + } + if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 0U) != + mms::MmsStaticBrcbStatus::temporarily_unavailable || + runtime.set_enabled(true) != mms::MmsStaticBrcbStatus::ok) { + return 2; + } + + if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 100U) != + mms::MmsStaticBrcbStatus::ok || + runtime.notify(2U, mms::MmsStaticBrcbEventReason::quality_change, 105U) != + mms::MmsStaticBrcbStatus::ok) { + return 3; + } + mms::MmsStaticBrcbCapturePlan plan; + if (runtime.next_due(119U, plan) || !runtime.next_due(120U, plan) || + plan.entry_number != 1U || plan.sequence_number != 1U || plan.buffer_overflow) { + return 4; + } + + std::array tiny{}; + std::array staging{}; + std::array workspace{}; + auto capture = runtime.capture(plan, kReportTime, tiny, workspace); + if (capture.status != mms::MmsStaticBrcbStatus::response_buffer_too_small || + capture.required_bytes <= tiny.size() || runtime.queue_size() != 0U || + !runtime.next_due(120U, plan) || plan.entry_number != 1U) { + return 5; + } + + capture = runtime.capture(plan, kReportTime, staging, workspace); + if (!capture.success() || runtime.queue_size() != 1U || runtime.retained_size() != 1U || + capture.included_member_count != 2U || !expected_entry(capture.entry_id, 1U)) { + return 6; + } + + mms::MmsStaticBrcbEntryView entry; + mms::MmsInformationReportView report; + if (!runtime.front(entry) || entry.sequence_number != 1U || entry.buffer_overflow || + !mms::MmsInformationReportSpanCodec::try_decode_information_report( + entry.mms_pdu, report) || report.item_count != 15U) { + return 7; + } + mms::MmsReadAccessResultView item; + bool boolean_value = false; + if (!report.try_item(5U, item) || !decode_boolean(item, boolean_value) || boolean_value || + !report.try_item(6U, item) || !decode_octet_string(item, capture.entry_id) || + !report.try_item(8U, item) || !decode_bit_string(item, 5U, 0xA0U) || + !report.try_item(11U, item) || !decode_boolean(item, boolean_value) || !boolean_value || + !report.try_item(12U, item) || !decode_boolean(item, boolean_value) || !boolean_value || + !report.try_item(13U, item) || !decode_bit_string(item, 2U, 0x80U) || + !report.try_item(14U, item) || !decode_bit_string(item, 2U, 0x40U)) { + return 8; + } + + if (runtime.notify(1U, mms::MmsStaticBrcbEventReason::data_update, 200U) != + mms::MmsStaticBrcbStatus::ok || + !runtime.next_due(220U, plan)) { + return 9; + } + capture = runtime.capture(plan, kReportTime, staging, workspace); + if (!capture.success() || runtime.queue_size() != 2U || runtime.retained_size() != 2U || + !expected_entry(capture.entry_id, 2U)) { + return 10; + } + + if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 300U) != + mms::MmsStaticBrcbStatus::ok || + !runtime.next_due(320U, plan) || !plan.buffer_overflow) { + return 11; + } + const auto stale_plan = plan; + const auto first_entry_id = std::array{ + 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U}; + if (runtime.commit_delivery(first_entry_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 1U || runtime.retained_size() != 2U || + runtime.capture(stale_plan, kReportTime, staging, workspace).status != + mms::MmsStaticBrcbStatus::stale_plan || + !runtime.next_due(320U, plan) || plan.buffer_overflow) { + return 12; + } + capture = runtime.capture(plan, kReportTime, staging, workspace); + if (!capture.success() || runtime.queue_size() != 2U || runtime.retained_size() != 2U || + !expected_entry(capture.entry_id, 3U) || runtime.dropped_reports() != 0U || + runtime.replay_gap()) { + return 13; + } + + if (runtime.notify(2U, mms::MmsStaticBrcbEventReason::quality_change, 400U) != + mms::MmsStaticBrcbStatus::ok || + !runtime.next_due(420U, plan) || !plan.buffer_overflow) { + return 14; + } + capture = runtime.capture(plan, kReportTime, staging, workspace); + if (!capture.success() || runtime.queue_size() != 2U || runtime.retained_size() != 2U || + !expected_entry(capture.entry_id, 4U) || runtime.dropped_reports() != 1U || + !runtime.replay_gap()) { + return 15; + } + + // Entry 2 was the oldest undelivered report and was lost. Entry 3 is now front. + if (!runtime.front(entry) || entry.entry_id.size() != 8U || entry.entry_id[7] != 3U || + runtime.commit_delivery(entry.entry_id) != mms::MmsStaticBrcbStatus::ok || + !runtime.front(entry) || entry.entry_id[7] != 4U || !entry.buffer_overflow || + !mms::MmsInformationReportSpanCodec::try_decode_information_report( + entry.mms_pdu, report) || + !report.try_item(5U, item) || !decode_boolean(item, boolean_value) || !boolean_value || + !report.try_item(6U, item)) { + return 16; + } + const std::array third_id{0U,0U,0U,0U,0U,0U,0U,3U}; + const std::array fourth_id{0U,0U,0U,0U,0U,0U,0U,4U}; + if (!decode_octet_string(item, fourth_id) || + runtime.commit_delivery(fourth_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 0U || runtime.retained_size() != 2U) { + return 17; + } + + // Delivered entries remain replayable until buffer eviction or PurgeBuf. + if (runtime.replay_from(fourth_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 1U || !runtime.front(entry) || entry.entry_id[7U] != 4U || + runtime.commit_delivery(fourth_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 0U || + runtime.rewind_to_oldest() != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 2U || !runtime.front(entry) || entry.entry_id[7U] != 3U || + runtime.resume_after(third_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 1U || !runtime.front(entry) || entry.entry_id[7U] != 4U) { + return 18; + } + const std::array missing_id{0U,0U,0U,0U,0U,0U,0U,2U}; + if (runtime.resume_after(missing_id) != mms::MmsStaticBrcbStatus::entry_not_found || + runtime.purge_buffer() != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 0U || runtime.retained_size() != 0U || runtime.replay_gap() || + runtime.front(entry)) { + return 19; + } + + // Repeated capture/delivery proves bounded retained-history behavior and EntryID progression. + for (std::uint32_t iteration = 0U; iteration < 10'000U; ++iteration) { + const auto now = static_cast(1'000U + (iteration * 25U)); + if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_update, now) != + mms::MmsStaticBrcbStatus::ok || + !runtime.next_due(now + 20U, plan)) { + return 20; + } + capture = runtime.capture(plan, kReportTime, staging, workspace); + if (!capture.success() || !runtime.front(entry) || + runtime.commit_delivery(entry.entry_id) != mms::MmsStaticBrcbStatus::ok || + runtime.queue_size() != 0U || runtime.retained_size() > runtime.queue_capacity()) { + return 21; + } + } + + if (runtime.set_enabled(false) != mms::MmsStaticBrcbStatus::ok || runtime.enabled() || + runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 999'999U) != + mms::MmsStaticBrcbStatus::temporarily_unavailable) { + return 22; + } + + return 0; +} \ No newline at end of file diff --git a/embedded/mms_brcb_lifecycle_hard_profile_smoke.cpp b/embedded/mms_brcb_lifecycle_hard_profile_smoke.cpp new file mode 100644 index 000000000..21aae5e2f --- /dev/null +++ b/embedded/mms_brcb_lifecycle_hard_profile_smoke.cpp @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/connection_runtime.hpp" +#include "ariec61850/mms/static_brcb_control.hpp" +#include "ariec61850/mms/static_dispatcher.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include + +namespace { +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; + +constexpr std::array kAssociationRequest{ + 0x0DU,0xB6U,0x05U,0x06U,0x13U,0x01U,0x00U,0x16U,0x01U,0x02U,0x14U,0x02U, + 0x00U,0x02U,0x33U,0x02U,0x00U,0x01U,0x34U,0x02U,0x00U,0x01U,0xC1U,0xA0U, + 0x31U,0x81U,0x9DU,0xA0U,0x03U,0x80U,0x01U,0x01U,0xA2U,0x81U,0x95U,0x81U, + 0x04U,0x00U,0x00U,0x00U,0x01U,0x82U,0x04U,0x00U,0x00U,0x00U,0x01U,0xA4U, + 0x23U,0x30U,0x0FU,0x02U,0x01U,0x01U,0x06U,0x04U,0x52U,0x01U,0x00U,0x01U, + 0x30U,0x04U,0x06U,0x02U,0x51U,0x01U,0x30U,0x10U,0x02U,0x01U,0x03U,0x06U, + 0x05U,0x28U,0xCAU,0x22U,0x02U,0x01U,0x30U,0x04U,0x06U,0x02U,0x51U,0x01U, + 0x61U,0x62U,0x30U,0x60U,0x02U,0x01U,0x01U,0xA0U,0x5BU,0x60U,0x59U,0xA1U, + 0x07U,0x06U,0x05U,0x28U,0xCAU,0x22U,0x02U,0x03U,0xA2U,0x07U,0x06U,0x05U, + 0x29U,0x01U,0x87U,0x67U,0x01U,0xA3U,0x03U,0x02U,0x01U,0x0CU,0xA6U,0x06U, + 0x06U,0x04U,0x29U,0x01U,0x87U,0x67U,0xA7U,0x03U,0x02U,0x01U,0x0CU,0xBEU, + 0x33U,0x28U,0x31U,0x06U,0x02U,0x51U,0x01U,0x02U,0x01U,0x03U,0xA0U,0x28U, + 0xA8U,0x26U,0x80U,0x03U,0x00U,0xFDU,0xE8U,0x81U,0x01U,0x0AU,0x82U,0x01U, + 0x0AU,0x83U,0x01U,0x05U,0xA4U,0x16U,0x80U,0x01U,0x01U,0x81U,0x03U,0x05U, + 0xF1U,0x00U,0x82U,0x0CU,0x03U,0xEEU,0x1CU,0x00U,0x00U,0x04U,0x08U,0x00U, + 0x00U,0x79U,0xEFU,0x18U}; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] std::uint64_t read_now(const void* context) noexcept { + return context == nullptr ? 0U : *static_cast(context); +} + +struct LifecycleSink final { + mms::MmsStaticBrcbControl* control{}; + std::size_t calls{}; + std::uint64_t last_association{}; + std::uint64_t last_now{}; +}; + +void on_association_closed( + void* raw_context, + const std::uint64_t association_id, + const std::uint64_t now_ms) noexcept { + auto* sink = static_cast(raw_context); + if (sink == nullptr) { + return; + } + ++sink->calls; + sink->last_association = association_id; + sink->last_now = now_ms; + if (sink->control != nullptr) { + sink->control->on_association_closed(association_id, now_ms); + } +} + +[[nodiscard]] mms::MmsStaticBrcbClientIdentity identity( + const std::uint64_t association, + const std::array& owner) noexcept { + mms::MmsStaticBrcbClientIdentity result; + result.association_id = association; + result.owner[0] = owner[0]; + result.owner[1] = owner[1]; + result.owner_size = owner.size(); + return result; +} + +[[nodiscard]] mms::MmsStaticConnectionPolicy policy( + const std::uint64_t association, + const std::array& owner, + std::uint64_t& now, + LifecycleSink& sink) noexcept { + mms::MmsStaticConnectionPolicy result; + result.association_id = association; + result.owner[0] = owner[0]; + result.owner[1] = owner[1]; + result.owner_size = owner.size(); + result.now_ms = read_now; + result.now_context = &now; + result.association_closed = on_association_closed; + result.association_closed_context = &sink; + return result; +} + +[[nodiscard]] bool establish( + mms::MmsStaticConnectionRuntime& connection, + std::span request, + std::span response, + std::span workspace, + std::span scratch) noexcept { + constexpr std::array tpdu_size{0x0AU}; + constexpr std::array source_tsap{0x00U, 0x01U}; + constexpr std::array destination_tsap{0x00U, 0x01U}; + const std::array parameters{ + osi::CotpParameterView{osi::CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + osi::CotpParameterView{osi::CotpSpanCodec::source_tsap_parameter, source_tsap}, + osi::CotpParameterView{osi::CotpSpanCodec::destination_tsap_parameter, destination_tsap}}; + + const auto cr = osi::CotpSpanCodec::encode_connection_request_into( + 0x0001U, parameters, scratch); + if (!cr.success()) { + return false; + } + const auto cr_tpkt = osi::TpktSpanCodec::encode_into( + scratch.first(cr.bytes_written), request); + if (!cr_tpkt.success()) { + return false; + } + auto result = connection.process_tcp_window( + request.first(cr_tpkt.bytes_written), response, workspace); + if (!result.response_ready() || + connection.state() != mms::MmsStaticConnectionState::awaiting_association) { + return false; + } + + const auto cotp = osi::CotpSpanCodec::encode_data_into(kAssociationRequest, scratch); + if (!cotp.success()) { + return false; + } + const auto association = osi::TpktSpanCodec::encode_into( + scratch.first(cotp.bytes_written), request); + if (!association.success()) { + return false; + } + result = connection.process_tcp_window( + request.first(association.bytes_written), response, workspace); + return result.response_ready() && + connection.state() == mms::MmsStaticConnectionState::established && + connection.association_active() && + connection.mms_presentation_context_id() == 3U; +} + +[[nodiscard]] bool send_disconnect_request( + mms::MmsStaticConnectionRuntime& connection, + std::span request, + std::span response, + std::span workspace) noexcept { + constexpr std::array cotp_disconnect{ + 0x06U, 0x80U, 0x00U, 0x00U, 0x00U, 0x01U, 0x00U}; + const auto tpkt = osi::TpktSpanCodec::encode_into(cotp_disconnect, request); + if (!tpkt.success()) { + return false; + } + const auto result = connection.process_tcp_window( + request.first(tpkt.bytes_written), response, workspace); + return result.status == mms::MmsStaticConnectionStatus::closed && + result.state == mms::MmsStaticConnectionState::closed && + result.consumed_bytes == tpkt.bytes_written && + !connection.association_active(); +} + +} // namespace + +int main() { + bool value = true; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "X1", kBooleanType, read_boolean, &value, false}}; + const mms::MmsStaticObjectTable object_table{objects}; + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + const mms::MmsStaticApplicationDispatcher dispatcher{object_table, data_set_table}; + + const mms::MmsStaticBrcbDefinition definition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Events", + "LD0", + "LLN0$Events", + 1U, + {0x7FU, 0x80U}, + 0U, + 0x70U}; + std::array slot_storage{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot_storage}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime reports{ + definition, pending, slots, object_table, data_set_table}; + if (!reports.initialize()) { + return 1; + } + mms::MmsStaticBrcbControl control{reports}; + LifecycleSink sink{&control}; + + const std::array owner_a{0xAAU, 0x01U}; + const std::array owner_b{0xBBU, 0x01U}; + const auto a1 = identity(101U, owner_a); + const auto a2 = identity(102U, owner_a); + const auto a3 = identity(103U, owner_a); + const auto b1 = identity(201U, owner_b); + + std::array request{}; + std::array response{}; + std::array workspace{}; + std::array scratch{}; + + std::uint64_t now = 100U; + mms::MmsStaticConnectionRuntime first{ + dispatcher, policy(a1.association_id, owner_a, now, sink)}; + if (!establish(first, request, response, workspace, scratch) || + control.reserve(a1, 5U, now) != mms::MmsStaticBrcbControlStatus::ok || + control.set_report_enabled(a1, true, now) != + mms::MmsStaticBrcbControlStatus::ok) { + return 2; + } + + // TCP EOF/socket/local close must notify exactly once and retain a timed + // reservation while always disabling report transmission. + now = 1'000U; + first.close_transport(); + auto state = control.state(now); + if (first.state() != mms::MmsStaticConnectionState::closed || + first.association_active() || sink.calls != 1U || + sink.last_association != 101U || sink.last_now != now || + reports.enabled() || !state.reserved || state.owner_connected || + state.association_id != 0U || state.reservation_expires_at_ms != 6'000U) { + return 3; + } + first.close_transport(); + if (sink.calls != 1U) { + return 4; + } + + now = 2'000U; + if (control.reserve(b1, 5U, now) != + mms::MmsStaticBrcbControlStatus::object_access_denied) { + return 5; + } + + // The same stable Owner may reconnect with a new ephemeral association. + now = 2'500U; + if (control.reserve(a2, 5U, now) != mms::MmsStaticBrcbControlStatus::ok) { + return 6; + } + mms::MmsStaticConnectionRuntime second{ + dispatcher, policy(a2.association_id, owner_a, now, sink)}; + if (!establish(second, request, response, workspace, scratch) || + control.set_report_enabled(a2, true, now) != + mms::MmsStaticBrcbControlStatus::ok) { + return 7; + } + + // Runtime reuse/reset is also a transport teardown and must deliver one + // lifecycle event before allowing a fresh COTP connection. + now = 3'000U; + second.reset(); + state = control.state(now); + if (sink.calls != 2U || sink.last_association != 102U || + sink.last_now != now || reports.enabled() || + second.state() != mms::MmsStaticConnectionState::awaiting_cotp_connect || + second.association_active() || !state.reserved || state.owner_connected || + state.reservation_expires_at_ms != 8'000U) { + return 8; + } + second.reset(); + if (sink.calls != 2U) { + return 9; + } + + // Reclaim with ResvTms=0 and prove an incoming COTP DR drives immediate + // release through the exact same generic lifecycle bridge. + now = 3'500U; + if (control.reserve(a3, 0U, now) != mms::MmsStaticBrcbControlStatus::ok) { + return 10; + } + mms::MmsStaticConnectionRuntime third{ + dispatcher, policy(a3.association_id, owner_a, now, sink)}; + if (!establish(third, request, response, workspace, scratch) || + control.set_report_enabled(a3, true, now) != + mms::MmsStaticBrcbControlStatus::ok) { + return 11; + } + + now = 3'600U; + if (!send_disconnect_request(third, request, response, workspace)) { + return 12; + } + state = control.state(now); + if (sink.calls != 3U || sink.last_association != 103U || + sink.last_now != now || reports.enabled() || state.reserved || + state.owner_connected || !state.owner.empty()) { + return 13; + } + third.close_transport(); + if (sink.calls != 3U) { + return 14; + } + + return 0; +} diff --git a/embedded/mms_brcb_objects_hard_profile_smoke.cpp b/embedded/mms_brcb_objects_hard_profile_smoke.cpp new file mode 100644 index 000000000..e1349f9be --- /dev/null +++ b/embedded/mms_brcb_objects_hard_profile_smoke.cpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/mms/static_brcb_objects.hpp" +#include "ariec61850/mms/static_dispatcher.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kTrue{0x83U, 0x01U, 0xFFU}; +constexpr std::array kFalse{0x83U, 0x01U, 0x00U}; +constexpr std::array kFiveSeconds{0x85U, 0x01U, 0x05U}; +constexpr std::array kZeroSeconds{0x85U, 0x01U, 0x00U}; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] std::uint64_t read_now(const void* context) noexcept { + return context == nullptr ? 0U : *static_cast(context); +} + +[[nodiscard]] mms::MmsStaticRequestAccessContext access( + const std::uint64_t association, + const std::span owner) noexcept { + return {association, owner}; +} + +[[nodiscard]] std::size_t build_write_request( + const std::uint8_t invoke, + const std::string_view domain, + const std::string_view item, + const std::span data, + const std::span destination) noexcept { + if (domain.size() > 0x7FU || item.size() > 0x7FU || data.size() > 0x7FU) { + return 0U; + } + const auto total = 24U + item.size() + data.size() + domain.size() - 3U; + if (total > destination.size() || total > 0x7FU) { + return 0U; + } + const auto confirmed_content = total - 2U; + const auto write_content = 14U + domain.size() + item.size() + data.size(); + const auto variable_list_content = 10U + domain.size() + item.size(); + const auto sequence_content = 8U + domain.size() + item.size(); + const auto variable_content = 6U + domain.size() + item.size(); + const auto domain_specific_content = 4U + domain.size() + item.size(); + + std::size_t offset = 0U; + const auto put = [&](const std::uint8_t byte) noexcept -> bool { + if (offset >= destination.size()) { + return false; + } + destination[offset++] = byte; + return true; + }; + if (!put(0xA0U) || !put(static_cast(confirmed_content)) || + !put(0x02U) || !put(0x01U) || !put(invoke) || + !put(0xA5U) || !put(static_cast(write_content)) || + !put(0xA0U) || !put(static_cast(variable_list_content)) || + !put(0x30U) || !put(static_cast(sequence_content)) || + !put(0xA0U) || !put(static_cast(variable_content)) || + !put(0xA1U) || !put(static_cast(domain_specific_content)) || + !put(0x1AU) || !put(static_cast(domain.size()))) { + return 0U; + } + for (const auto ch : domain) { + if (!put(static_cast(static_cast(ch)))) { + return 0U; + } + } + if (!put(0x1AU) || !put(static_cast(item.size()))) { + return 0U; + } + for (const auto ch : item) { + if (!put(static_cast(static_cast(ch)))) { + return 0U; + } + } + if (!put(0xA0U) || !put(static_cast(data.size()))) { + return 0U; + } + for (const auto byte : data) { + if (!put(byte)) { + return 0U; + } + } + return offset == total ? offset : 0U; +} + +[[nodiscard]] bool dispatch_write( + const mms::MmsStaticApplicationDispatcher& dispatcher, + const std::string_view item, + const std::span data, + const mms::MmsStaticRequestAccessContext& client, + const bool expected_success, + const std::uint32_t expected_failure, + std::uint8_t& invoke) noexcept { + std::array request{}; + std::array response{}; + std::array workspace{}; + const auto bytes = build_write_request( + invoke++, "LD0", item, data, request); + if (bytes == 0U) { + return false; + } + const auto dispatched = dispatcher.dispatch( + std::span{request}.first(bytes), + response, + workspace, + client); + if (!dispatched.success()) { + return false; + } + mms::MmsWriteResponseView decoded; + mms::MmsWriteAccessResultView result; + if (!mms::MmsServiceSpanCodec::try_decode_write_response( + std::span{response}.first(dispatched.bytes_written), + decoded) || + decoded.result_count != 1U || !decoded.try_result(0U, result)) { + return false; + } + return expected_success + ? result.success + : !result.success && result.failure_code == expected_failure; +} + +[[nodiscard]] std::array octet_data( + const std::span value) noexcept { + std::array encoded{}; + if (value.size() != 8U) { + return encoded; + } + encoded[0] = 0x89U; + encoded[1] = 0x08U; + std::copy(value.begin(), value.end(), encoded.begin() + 2); + return encoded; +} + +} // namespace + +int main() { + bool value = true; + const std::array base_objects{ + mms::MmsStaticObjectEntry{ + "LD0", "X1", kBooleanType, read_boolean, &value, false}}; + const mms::MmsStaticObjectTable base_table{base_objects}; + + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + + const mms::MmsStaticBrcbDefinition definition{ + "LD0", + "B1", + "LD0/LLN0$BR$B1", + "LD0", + "LLN0$Events", + 1U, + {0x7FU, 0x80U}, + 0U, + 0x70U}; + + std::array slot0{}; + std::array slot1{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}, + mms::MmsStaticBrcbSlot{slot1}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime reports{ + definition, pending, slots, base_table, data_set_table}; + if (!reports.initialize()) { + return 1; + } + mms::MmsStaticBrcbControl control{reports}; + + std::array object_storage{}; + std::array context_storage{}; + std::array name_storage{}; + std::uint64_t now = 100U; + mms::MmsStaticBrcbObjectBank bank{ + definition, + reports, + control, + base_objects, + object_storage, + context_storage, + name_storage, + read_now, + &now}; + if (!bank.initialize() || bank.object_count() != 9U || !bank.table().valid()) { + return 2; + } + + const mms::MmsStaticApplicationDispatcher dispatcher{bank.table(), data_set_table}; + const std::array owner_a{0xAAU, 0x01U}; + const std::array owner_b{0xBBU, 0x01U}; + const auto a = access(101U, owner_a); + const auto b = access(201U, owner_b); + std::uint8_t invoke = 1U; + + if (!dispatch_write(dispatcher, "B1$RptEna", kTrue, a, true, 0U, invoke) || + !reports.enabled()) { + return 3; + } + auto state = control.state(now); + if (!state.reserved || !state.owner_connected || + state.association_id != 101U || state.owner.size() != owner_a.size() || + !std::equal(state.owner.begin(), state.owner.end(), owner_a.begin())) { + return 4; + } + + if (!dispatch_write(dispatcher, "B1$RptEna", kTrue, b, false, 3U, invoke) || + !dispatch_write(dispatcher, "B1$PurgeBuf", kTrue, b, false, 3U, invoke)) { + return 5; + } + + std::array encode_buffer{}; + std::array capture_workspace{}; + const std::array report_time{0U,0U,0x12U,0x34U,0U,0x01U}; + if (reports.notify(0U, mms::MmsStaticBrcbEventReason::data_change, now) != + mms::MmsStaticBrcbStatus::ok) { + return 6; + } + mms::MmsStaticBrcbCapturePlan plan; + if (!reports.next_due(now, plan)) { + return 7; + } + const auto first = reports.capture(plan, report_time, encode_buffer, capture_workspace); + if (!first.success()) { + return 8; + } + + value = false; + now = 200U; + if (reports.notify(0U, mms::MmsStaticBrcbEventReason::data_change, now) != + mms::MmsStaticBrcbStatus::ok || + !reports.next_due(now, plan)) { + return 9; + } + const auto second = reports.capture(plan, report_time, encode_buffer, capture_workspace); + if (!second.success() || reports.retained_size() != 2U || + reports.latest_entry_id() != second.entry_id) { + return 10; + } + + if (!dispatch_write(dispatcher, "B1$RptEna", kFalse, a, true, 0U, invoke) || + reports.enabled()) { + return 11; + } + + if (!dispatch_write(dispatcher, "B1$ResvTms", kFiveSeconds, a, true, 0U, invoke)) { + return 12; + } + state = control.state(now); + if (!state.reserved || state.resv_tms_seconds != 5U || + state.owner.size() != owner_a.size()) { + return 13; + } + + const auto first_entry_data = octet_data(first.entry_id); + if (!dispatch_write( + dispatcher, "B1$EntryID", first_entry_data, a, true, 0U, invoke) || + reports.queue_size() != 1U) { + return 14; + } + mms::MmsStaticBrcbEntryView front; + if (!reports.front(front) || front.entry_id.size() != second.entry_id.size() || + !std::equal(front.entry_id.begin(), front.entry_id.end(), second.entry_id.begin())) { + return 15; + } + + const std::array zero_entry{}; + const auto zero_entry_data = octet_data(zero_entry); + if (!dispatch_write( + dispatcher, "B1$EntryID", zero_entry_data, a, true, 0U, invoke) || + reports.queue_size() != 2U) { + return 16; + } + const std::array missing_entry{0U,0U,0U,0U,0U,0U,0U,0x7FU}; + const auto missing_entry_data = octet_data(missing_entry); + if (!dispatch_write( + dispatcher, "B1$EntryID", missing_entry_data, a, false, 11U, invoke)) { + return 17; + } + + if (!dispatch_write(dispatcher, "B1$PurgeBuf", kTrue, a, true, 0U, invoke) || + reports.retained_size() != 0U || reports.queue_size() != 0U || + reports.latest_entry_id() != zero_entry) { + return 18; + } + + if (!dispatch_write(dispatcher, "B1$ResvTms", kZeroSeconds, a, true, 0U, invoke)) { + return 19; + } + state = control.state(now); + if (state.reserved || !state.owner.empty()) { + return 20; + } + + std::array read_buffer{}; + const auto& entry_object = object_storage[1U + 5U]; + const auto entry_read = entry_object.read(entry_object.context, read_buffer); + if (!entry_read.success() || entry_read.bytes_written != 10U || + read_buffer[0] != 0x89U || read_buffer[1] != 0x08U || + !std::all_of(read_buffer.begin() + 2, read_buffer.begin() + 10, + [](const std::uint8_t byte) { return byte == 0U; })) { + return 21; + } + const auto& purge_object = object_storage[1U + 4U]; + const auto purge_read = purge_object.read(purge_object.context, read_buffer); + if (!purge_read.success() || purge_read.bytes_written != 3U || + read_buffer[0] != 0x83U || read_buffer[2] != 0x00U) { + return 22; + } + + return 0; +} diff --git a/embedded/mms_brcb_persistence_hard_profile_smoke.cpp b/embedded/mms_brcb_persistence_hard_profile_smoke.cpp new file mode 100644 index 000000000..98b0cd3e4 --- /dev/null +++ b/embedded/mms_brcb_persistence_hard_profile_smoke.cpp @@ -0,0 +1,373 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_checkpoint_store.hpp" +#include "ariec61850/mms/static_brcb_runtime.hpp" +#include "ariec61850/mms/static_brcb_state_codec.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kReportTime{ + 0x00U, 0x00U, 0x12U, 0x34U, 0x00U, 0x01U}; +constexpr std::size_t kBankBytes = 4096U; +constexpr std::size_t kStorageBytes = kBankBytes * 2U; + +bool g_value = true; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +const std::array kObjects{ + mms::MmsStaticObjectEntry{"LD0", "X1", kBooleanType, read_boolean, &g_value, false}}; +const mms::MmsStaticObjectTable kObjectTable{kObjects}; +const std::array kMembers{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; +const std::array kDataSets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", kMembers, false}}; +const mms::MmsStaticDataSetTable kDataSetTable{kDataSets}; + +const mms::MmsStaticBrcbDefinition kDefinition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Events", + "LD0", + "LLN0$Events", + 17U, + {0x7FU, 0x80U}, + 20U, + 0x70U}; + +const mms::MmsStaticBrcbDefinition kDifferentDefinition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Different", + "LD0", + "LLN0$Events", + 17U, + {0x7FU, 0x80U}, + 20U, + 0x70U}; + +struct RuntimeFixture final { + std::array slot0{}; + std::array slot1{}; + std::array slot2{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}, + mms::MmsStaticBrcbSlot{slot1}, + mms::MmsStaticBrcbSlot{slot2}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime runtime; + + explicit RuntimeFixture(const mms::MmsStaticBrcbDefinition& definition) noexcept + : runtime{definition, pending, slots, kObjectTable, kDataSetTable} {} +}; + +struct SmallRuntimeFixture final { + std::array slot0{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime runtime{ + kDefinition, pending, slots, kObjectTable, kDataSetTable}; +}; + +struct FakeStorage final { + std::array bytes{}; + std::size_t write_budget{std::numeric_limits::max()}; + bool fail_reads{}; + bool fail_erases{}; + bool fail_sync{}; + + FakeStorage() noexcept { + std::fill(bytes.begin(), bytes.end(), std::uint8_t{0xFFU}); + } +}; + +[[nodiscard]] bool storage_read( + void* context, + const std::size_t offset, + const std::span destination) noexcept { + auto* storage = static_cast(context); + if (storage == nullptr || storage->fail_reads || offset > storage->bytes.size() || + destination.size() > storage->bytes.size() - offset) { + return false; + } + std::copy_n(storage->bytes.begin() + offset, destination.size(), destination.begin()); + return true; +} + +[[nodiscard]] bool storage_write( + void* context, + const std::size_t offset, + const std::span source) noexcept { + auto* storage = static_cast(context); + if (storage == nullptr || offset > storage->bytes.size() || + source.size() > storage->bytes.size() - offset) { + return false; + } + const auto writable = std::min(source.size(), storage->write_budget); + std::copy_n(source.begin(), writable, storage->bytes.begin() + offset); + if (storage->write_budget != std::numeric_limits::max()) { + storage->write_budget -= writable; + } + return writable == source.size(); +} + +[[nodiscard]] bool storage_erase( + void* context, + const std::size_t offset, + const std::size_t bytes) noexcept { + auto* storage = static_cast(context); + if (storage == nullptr || storage->fail_erases || offset > storage->bytes.size() || + bytes > storage->bytes.size() - offset) { + return false; + } + std::fill_n(storage->bytes.begin() + offset, bytes, std::uint8_t{0xFFU}); + return true; +} + +[[nodiscard]] bool storage_sync(void* context) noexcept { + const auto* storage = static_cast(context); + return storage != nullptr && !storage->fail_sync; +} + +[[nodiscard]] bool capture_one( + mms::MmsStaticBrcbRuntime& runtime, + const std::uint64_t now_ms, + const std::span staging, + const std::span workspace) noexcept { + if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, now_ms) != + mms::MmsStaticBrcbStatus::ok) { + return false; + } + mms::MmsStaticBrcbCapturePlan plan; + if (!runtime.next_due(now_ms + 20U, plan)) { + return false; + } + return runtime.capture(plan, kReportTime, staging, workspace).success(); +} + +[[nodiscard]] bool entry_is( + const mms::MmsStaticBrcbEntryView& entry, + const std::uint8_t low_byte) noexcept { + return entry.entry_id.size() == 8U && entry.entry_id[7U] == low_byte; +} + +} // namespace + +int main() { + FakeStorage storage; + const mms::MmsStaticBrcbStorageBackend backend{ + &storage, + storage.bytes.size(), + storage_read, + storage_write, + storage_erase, + storage_sync}; + const mms::MmsStaticBrcbCheckpointStore store{backend, kBankBytes}; + if (!store.valid() || store.maximum_state_bytes() == 0U) { + return 1; + } + + std::array state_buffer{}; + std::array staging{}; + std::array workspace{}; + + RuntimeFixture empty{kDefinition}; + if (!empty.runtime.initialize() || + store.restore(empty.runtime, state_buffer).status != + mms::MmsStaticBrcbCheckpointStatus::no_checkpoint) { + return 2; + } + + RuntimeFixture source{kDefinition}; + if (!source.runtime.initialize() || + source.runtime.set_enabled(true) != mms::MmsStaticBrcbStatus::ok) { + return 3; + } + g_value = true; + if (!capture_one(source.runtime, 100U, staging, workspace)) { + return 4; + } + g_value = false; + if (!capture_one(source.runtime, 200U, staging, workspace) || + source.runtime.queue_size() != 2U) { + return 5; + } + + mms::MmsStaticBrcbEntryView source_front; + std::array expected_front{}; + std::array expected_entry_id{}; + if (!source.runtime.front(source_front) || source_front.mms_pdu.size() > expected_front.size()) { + return 6; + } + const auto expected_front_bytes = source_front.mms_pdu.size(); + std::copy(source_front.mms_pdu.begin(), source_front.mms_pdu.end(), expected_front.begin()); + std::copy(source_front.entry_id.begin(), source_front.entry_id.end(), expected_entry_id.begin()); + + std::array tiny_state{}; + const auto tiny_checkpoint = store.checkpoint(source.runtime, tiny_state); + if (tiny_checkpoint.status != mms::MmsStaticBrcbCheckpointStatus::state_buffer_too_small || + tiny_checkpoint.required_state_bytes <= tiny_state.size()) { + return 7; + } + + const auto first_checkpoint = store.checkpoint(source.runtime, state_buffer); + if (!first_checkpoint.success() || first_checkpoint.generation != 1U || + first_checkpoint.bank_index != 0U || first_checkpoint.state_bytes == 0U) { + return 8; + } + + RuntimeFixture recovered{kDefinition}; + if (!recovered.runtime.initialize()) { + return 9; + } + const auto first_restore = store.restore(recovered.runtime, state_buffer); + mms::MmsStaticBrcbEntryView recovered_front; + if (!first_restore.success() || first_restore.generation != 1U || + recovered.runtime.enabled() || recovered.runtime.queue_size() != 2U || + !recovered.runtime.front(recovered_front) || + recovered_front.mms_pdu.size() != expected_front_bytes || + !std::equal( + recovered_front.mms_pdu.begin(), + recovered_front.mms_pdu.end(), + expected_front.begin()) || + !std::equal( + recovered_front.entry_id.begin(), + recovered_front.entry_id.end(), + expected_entry_id.begin())) { + return 10; + } + + RuntimeFixture wrong_definition{kDifferentDefinition}; + if (!wrong_definition.runtime.initialize()) { + return 11; + } + const auto mismatch = store.restore(wrong_definition.runtime, state_buffer); + if (mismatch.status != mms::MmsStaticBrcbCheckpointStatus::state_restore_failed || + mismatch.state_status != mms::MmsStaticBrcbStateStatus::definition_mismatch || + wrong_definition.runtime.queue_size() != 0U) { + return 12; + } + + SmallRuntimeFixture too_small; + if (!too_small.runtime.initialize()) { + return 13; + } + const auto capacity = store.restore(too_small.runtime, state_buffer); + if (capacity.status != mms::MmsStaticBrcbCheckpointStatus::state_restore_failed || + capacity.state_status != mms::MmsStaticBrcbStateStatus::capacity_mismatch || + too_small.runtime.queue_size() != 0U) { + return 14; + } + + // Build generation 2: deliver EntryID 1, then append EntryID 3. + if (!entry_is(recovered_front, 1U) || + recovered.runtime.commit_delivery(recovered_front.entry_id) != + mms::MmsStaticBrcbStatus::ok || + recovered.runtime.set_enabled(true) != mms::MmsStaticBrcbStatus::ok) { + return 15; + } + g_value = true; + if (!capture_one(recovered.runtime, 300U, staging, workspace) || + recovered.runtime.queue_size() != 2U || + !recovered.runtime.front(recovered_front) || !entry_is(recovered_front, 2U)) { + return 16; + } + const auto second_checkpoint = store.checkpoint(recovered.runtime, state_buffer); + if (!second_checkpoint.success() || second_checkpoint.generation != 2U || + second_checkpoint.bank_index != 1U) { + return 17; + } + + // Queue now advances to EntryID 4, but checkpoint generation 3 is torn + // while writing the final commit footer. Generation 2 must survive reboot. + g_value = false; + if (!capture_one(recovered.runtime, 400U, staging, workspace) || + recovered.runtime.queue_size() != 3U) { + return 18; + } + const auto projected = mms::MmsStaticBrcbStateCodec::encode( + recovered.runtime, state_buffer); + if (!projected.success()) { + return 19; + } + storage.write_budget = + mms::MmsStaticBrcbCheckpointStore::record_header_bytes + + projected.bytes_written + 8U; + const auto torn = store.checkpoint(recovered.runtime, state_buffer); + if (torn.status != mms::MmsStaticBrcbCheckpointStatus::storage_failure || + torn.generation != 3U || torn.bank_index != 0U) { + return 20; + } + storage.write_budget = std::numeric_limits::max(); + + RuntimeFixture after_torn{kDefinition}; + if (!after_torn.runtime.initialize()) { + return 21; + } + const auto torn_restore = store.restore(after_torn.runtime, state_buffer); + mms::MmsStaticBrcbEntryView torn_front; + if (!torn_restore.success() || torn_restore.generation != 2U || + torn_restore.bank_index != 1U || after_torn.runtime.queue_size() != 2U || + !after_torn.runtime.front(torn_front) || !entry_is(torn_front, 2U)) { + return 22; + } + + // Retry generation 3 successfully. The store alternates back to bank 0. + const auto third_checkpoint = store.checkpoint(recovered.runtime, state_buffer); + if (!third_checkpoint.success() || third_checkpoint.generation != 3U || + third_checkpoint.bank_index != 0U) { + return 23; + } + + // Corrupt the newest payload. Recovery must reject bank 0 by CRC and fall + // back to still-valid generation 2 in bank 1. + storage.bytes[mms::MmsStaticBrcbCheckpointStore::record_header_bytes + 10U] ^= + std::uint8_t{0x01U}; + RuntimeFixture after_corruption{kDefinition}; + if (!after_corruption.runtime.initialize()) { + return 24; + } + const auto corrupt_restore = store.restore(after_corruption.runtime, state_buffer); + if (!corrupt_restore.success() || corrupt_restore.generation != 2U || + corrupt_restore.bank_index != 1U || after_corruption.runtime.queue_size() != 2U || + !after_corruption.runtime.front(torn_front) || !entry_is(torn_front, 2U)) { + return 25; + } + + // Erasing both banks produces a clean no-checkpoint state, not partial data. + std::fill(storage.bytes.begin(), storage.bytes.end(), std::uint8_t{0xFFU}); + RuntimeFixture erased{kDefinition}; + if (!erased.runtime.initialize() || + store.restore(erased.runtime, state_buffer).status != + mms::MmsStaticBrcbCheckpointStatus::no_checkpoint || + erased.runtime.queue_size() != 0U) { + return 26; + } + + return 0; +} diff --git a/embedded/mms_brcb_recovery_v2_hard_profile_smoke.cpp b/embedded/mms_brcb_recovery_v2_hard_profile_smoke.cpp new file mode 100644 index 000000000..9a8123661 --- /dev/null +++ b/embedded/mms_brcb_recovery_v2_hard_profile_smoke.cpp @@ -0,0 +1,273 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_runtime.hpp" +#include "ariec61850/mms/static_brcb_state_codec.hpp" + +#include +#include +#include +#include +#include + +namespace { +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kReportTime{ + 0x00U, 0x00U, 0x12U, 0x34U, 0x00U, 0x01U}; + +bool g_value = true; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +const std::array kObjects{ + mms::MmsStaticObjectEntry{ + "LD0", "X1", kBooleanType, read_boolean, &g_value, false}}; +const mms::MmsStaticObjectTable kObjectTable{kObjects}; +const std::array kMembers{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; +const std::array kDataSets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", kMembers, false}}; +const mms::MmsStaticDataSetTable kDataSetTable{kDataSets}; + +const mms::MmsStaticBrcbDefinition kDefinition{ + "LD0", + "LLN0$BR$Events", + "LD0/LLN0$BR$Events", + "LD0", + "LLN0$Events", + 23U, + {0x7FU, 0x80U}, + 0U, + 0x70U}; + +struct RuntimeFixture final { + std::array slot0{}; + std::array slot1{}; + std::array slot2{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot0}, + mms::MmsStaticBrcbSlot{slot1}, + mms::MmsStaticBrcbSlot{slot2}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime runtime{ + kDefinition, pending, slots, kObjectTable, kDataSetTable}; +}; + +[[nodiscard]] bool capture_one( + mms::MmsStaticBrcbRuntime& runtime, + const std::uint64_t now_ms, + const std::span staging, + const std::span workspace) noexcept { + if (runtime.notify(0U, mms::MmsStaticBrcbEventReason::data_change, now_ms) != + mms::MmsStaticBrcbStatus::ok) { + return false; + } + mms::MmsStaticBrcbCapturePlan plan; + if (!runtime.next_due(now_ms, plan)) { + return false; + } + return runtime.capture(plan, kReportTime, staging, workspace).success(); +} + +[[nodiscard]] bool entry_is( + const mms::MmsStaticBrcbEntryView& entry, + const std::uint8_t low_byte) noexcept { + return entry.entry_id.size() == 8U && entry.entry_id[7U] == low_byte; +} + +[[nodiscard]] bool build_legacy_v1_from_v2( + const std::span v2, + const std::span legacy, + std::size_t& legacy_bytes) noexcept { + constexpr std::size_t v1_header = 48U; + constexpr std::size_t v2_header = 56U; + if (v2.size() < v2_header || legacy.size() < v2.size() - 8U || + v2[0] != 'A' || v2[1] != 'R' || v2[7] != '2') { + legacy_bytes = 0U; + return false; + } + + legacy_bytes = v2.size() - (v2_header - v1_header); + std::fill(legacy.begin(), legacy.begin() + static_cast(legacy_bytes), + std::uint8_t{0U}); + std::copy_n(v2.begin(), 41U, legacy.begin()); + legacy[7U] = static_cast('1'); + legacy[8U] = 0U; + legacy[9U] = 1U; + legacy[10U] = 0U; + legacy[11U] = static_cast(v1_header); + std::copy( + v2.begin() + static_cast(v2_header), + v2.end(), + legacy.begin() + static_cast(v1_header)); + return true; +} + +} // namespace + +int main() { + std::array state{}; + std::array legacy{}; + std::array staging{}; + std::array workspace{}; + + // v2 must preserve delivered retained history separately from the delivery + // cursor. Normal post-reboot delivery resumes at E2, while replay can still + // seek back to the already-delivered E1. + RuntimeFixture source; + if (!source.runtime.initialize() || + source.runtime.set_enabled(true) != mms::MmsStaticBrcbStatus::ok) { + return 1; + } + for (std::uint64_t now = 100U; now <= 300U; now += 100U) { + g_value = !g_value; + if (!capture_one(source.runtime, now, staging, workspace)) { + return 2; + } + } + mms::MmsStaticBrcbEntryView front; + std::array entry1{}; + if (source.runtime.retained_size() != 3U || source.runtime.queue_size() != 3U || + !source.runtime.front(front) || !entry_is(front, 1U)) { + return 3; + } + std::copy(front.entry_id.begin(), front.entry_id.end(), entry1.begin()); + if (source.runtime.commit_delivery(front.entry_id) != mms::MmsStaticBrcbStatus::ok || + source.runtime.retained_size() != 3U || source.runtime.queue_size() != 2U || + !source.runtime.front(front) || !entry_is(front, 2U)) { + return 4; + } + + const auto encoded = mms::MmsStaticBrcbStateCodec::encode(source.runtime, state); + if (!encoded.success() || encoded.bytes_written == 0U || + state[7U] != static_cast('2') || + state[9U] != mms::MmsStaticBrcbStateCodec::format_version) { + return 5; + } + + RuntimeFixture recovered; + if (!recovered.runtime.initialize()) { + return 6; + } + const auto restored = mms::MmsStaticBrcbStateCodec::restore( + recovered.runtime, + std::span{state}.first(encoded.bytes_written)); + if (!restored.success() || recovered.runtime.enabled() || + recovered.runtime.retained_size() != 3U || recovered.runtime.queue_size() != 2U || + recovered.runtime.replay_gap() || !recovered.runtime.front(front) || + !entry_is(front, 2U)) { + return 7; + } + if (recovered.runtime.rewind_to_oldest() != mms::MmsStaticBrcbStatus::ok || + recovered.runtime.queue_size() != 3U || !recovered.runtime.front(front) || + !entry_is(front, 1U) || + recovered.runtime.resume_after(entry1) != mms::MmsStaticBrcbStatus::ok || + recovered.runtime.queue_size() != 2U || !recovered.runtime.front(front) || + !entry_is(front, 2U)) { + return 8; + } + + // Backward restore: a v1 image has no delivered-history cursor, so it is + // intentionally interpreted as an all-undelivered queue. + RuntimeFixture legacy_source; + if (!legacy_source.runtime.initialize() || + legacy_source.runtime.set_enabled(true) != mms::MmsStaticBrcbStatus::ok || + !capture_one(legacy_source.runtime, 100U, staging, workspace) || + !capture_one(legacy_source.runtime, 200U, staging, workspace)) { + return 9; + } + const auto legacy_v2 = mms::MmsStaticBrcbStateCodec::encode( + legacy_source.runtime, state); + std::size_t legacy_bytes{}; + if (!legacy_v2.success() || + !build_legacy_v1_from_v2( + std::span{state}.first(legacy_v2.bytes_written), + legacy, + legacy_bytes)) { + return 10; + } + RuntimeFixture from_v1; + if (!from_v1.runtime.initialize() || + !mms::MmsStaticBrcbStateCodec::restore( + from_v1.runtime, + std::span{legacy}.first(legacy_bytes)).success() || + from_v1.runtime.retained_size() != 2U || from_v1.runtime.queue_size() != 2U || + from_v1.runtime.replay_gap() || !from_v1.runtime.front(front) || + !entry_is(front, 1U)) { + return 11; + } + + // Overflow creates a replay gap. v2 must retain that fact across reboot so + // an MMS client is not given false confidence that older history exists. + RuntimeFixture overflow; + if (!overflow.runtime.initialize() || + overflow.runtime.set_enabled(true) != mms::MmsStaticBrcbStatus::ok) { + return 12; + } + for (std::uint64_t now = 100U; now <= 400U; now += 100U) { + g_value = !g_value; + if (!capture_one(overflow.runtime, now, staging, workspace)) { + return 13; + } + } + if (overflow.runtime.retained_size() != 3U || overflow.runtime.queue_size() != 3U || + overflow.runtime.dropped_reports() != 1U || !overflow.runtime.replay_gap() || + !overflow.runtime.front(front) || !entry_is(front, 2U)) { + return 14; + } + const auto overflow_state = mms::MmsStaticBrcbStateCodec::encode( + overflow.runtime, state); + RuntimeFixture overflow_recovered; + if (!overflow_state.success() || !overflow_recovered.runtime.initialize() || + !mms::MmsStaticBrcbStateCodec::restore( + overflow_recovered.runtime, + std::span{state}.first(overflow_state.bytes_written)).success() || + overflow_recovered.runtime.retained_size() != 3U || + overflow_recovered.runtime.queue_size() != 3U || + overflow_recovered.runtime.dropped_reports() != 1U || + !overflow_recovered.runtime.replay_gap() || + !overflow_recovered.runtime.front(front) || !entry_is(front, 2U)) { + return 15; + } + + // Purge is a durable recovery boundary. It clears retained history/gap but + // does not roll EntryID backward; after another reboot the next report is E5. + if (overflow_recovered.runtime.purge_buffer() != mms::MmsStaticBrcbStatus::ok || + overflow_recovered.runtime.retained_size() != 0U || + overflow_recovered.runtime.queue_size() != 0U || + overflow_recovered.runtime.replay_gap()) { + return 16; + } + const auto purged_state = mms::MmsStaticBrcbStateCodec::encode( + overflow_recovered.runtime, state); + RuntimeFixture purged_recovered; + if (!purged_state.success() || !purged_recovered.runtime.initialize() || + !mms::MmsStaticBrcbStateCodec::restore( + purged_recovered.runtime, + std::span{state}.first(purged_state.bytes_written)).success() || + purged_recovered.runtime.retained_size() != 0U || + purged_recovered.runtime.queue_size() != 0U || + purged_recovered.runtime.replay_gap() || + purged_recovered.runtime.set_enabled(true) != mms::MmsStaticBrcbStatus::ok || + !capture_one(purged_recovered.runtime, 500U, staging, workspace) || + !purged_recovered.runtime.front(front) || !entry_is(front, 5U)) { + return 17; + } + + return 0; +} diff --git a/embedded/mms_connection_runtime_hard_profile_smoke.cpp b/embedded/mms_connection_runtime_hard_profile_smoke.cpp new file mode 100644 index 000000000..b2f05de1d --- /dev/null +++ b/embedded/mms_connection_runtime_hard_profile_smoke.cpp @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/connection_runtime.hpp" +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/mms/static_object_table.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kInt32Type{0x85U, 0x01U, 0x20U}; + +constexpr std::array kAssociationRequest{ + 0x0DU, 0xB6U, 0x05U, 0x06U, 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U, 0x14U, 0x02U, + 0x00U, 0x02U, 0x33U, 0x02U, 0x00U, 0x01U, 0x34U, 0x02U, 0x00U, 0x01U, 0xC1U, 0xA0U, + 0x31U, 0x81U, 0x9DU, 0xA0U, 0x03U, 0x80U, 0x01U, 0x01U, 0xA2U, 0x81U, 0x95U, 0x81U, + 0x04U, 0x00U, 0x00U, 0x00U, 0x01U, 0x82U, 0x04U, 0x00U, 0x00U, 0x00U, 0x01U, 0xA4U, + 0x23U, 0x30U, 0x0FU, 0x02U, 0x01U, 0x01U, 0x06U, 0x04U, 0x52U, 0x01U, 0x00U, 0x01U, + 0x30U, 0x04U, 0x06U, 0x02U, 0x51U, 0x01U, 0x30U, 0x10U, 0x02U, 0x01U, 0x03U, 0x06U, + 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, 0x01U, 0x30U, 0x04U, 0x06U, 0x02U, 0x51U, 0x01U, + 0x61U, 0x62U, 0x30U, 0x60U, 0x02U, 0x01U, 0x01U, 0xA0U, 0x5BU, 0x60U, 0x59U, 0xA1U, + 0x07U, 0x06U, 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, 0x03U, 0xA2U, 0x07U, 0x06U, 0x05U, + 0x29U, 0x01U, 0x87U, 0x67U, 0x01U, 0xA3U, 0x03U, 0x02U, 0x01U, 0x0CU, 0xA6U, 0x06U, + 0x06U, 0x04U, 0x29U, 0x01U, 0x87U, 0x67U, 0xA7U, 0x03U, 0x02U, 0x01U, 0x0CU, 0xBEU, + 0x33U, 0x28U, 0x31U, 0x06U, 0x02U, 0x51U, 0x01U, 0x02U, 0x01U, 0x03U, 0xA0U, 0x28U, + 0xA8U, 0x26U, 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, 0x81U, 0x01U, 0x0AU, 0x82U, 0x01U, + 0x0AU, 0x83U, 0x01U, 0x05U, 0xA4U, 0x16U, 0x80U, 0x01U, 0x01U, 0x81U, 0x03U, 0x05U, + 0xF1U, 0x00U, 0x82U, 0x0CU, 0x03U, 0xEEU, 0x1CU, 0x00U, 0x00U, 0x04U, 0x08U, 0x00U, + 0x00U, 0x79U, 0xEFU, 0x18U}; + +constexpr std::array kAssociationResponse{ + 0x0EU, 0x88U, 0x05U, 0x06U, 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U, 0x14U, 0x02U, + 0x00U, 0x02U, 0x33U, 0x02U, 0x00U, 0x01U, 0x34U, 0x02U, 0x00U, 0x01U, 0xC1U, 0x72U, + 0x31U, 0x70U, 0xA0U, 0x03U, 0x80U, 0x01U, 0x01U, 0xA2U, 0x69U, 0xA5U, 0x12U, 0x30U, + 0x07U, 0x80U, 0x01U, 0x00U, 0x81U, 0x02U, 0x51U, 0x01U, 0x30U, 0x07U, 0x80U, 0x01U, + 0x00U, 0x81U, 0x02U, 0x51U, 0x01U, 0x61U, 0x53U, 0x30U, 0x51U, 0x02U, 0x01U, 0x01U, + 0xA0U, 0x4CU, 0x61U, 0x4AU, 0xA1U, 0x07U, 0x06U, 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, + 0x03U, 0xA2U, 0x03U, 0x02U, 0x01U, 0x00U, 0xA3U, 0x05U, 0xA1U, 0x03U, 0x02U, 0x01U, + 0x00U, 0xBEU, 0x33U, 0x28U, 0x31U, 0x06U, 0x02U, 0x51U, 0x01U, 0x02U, 0x01U, 0x03U, + 0xA0U, 0x28U, 0xA9U, 0x26U, 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, 0x83U, 0x01U, 0x05U, 0xA4U, 0x16U, 0x80U, 0x01U, 0x01U, 0x81U, + 0x03U, 0x05U, 0xF1U, 0x00U, 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +constexpr std::array kReadRequest{ + 0xA0U, 0x27U, 0x02U, 0x01U, 0x0CU, + 0xA4U, 0x22U, 0xA1U, 0x20U, 0xA0U, 0x1EU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x4DU, 0x31U}; + +constexpr std::array kReadResponse{ + 0xA1U, 0x0DU, 0x02U, 0x01U, 0x0CU, + 0xA4U, 0x08U, 0xA1U, 0x06U, + 0x83U, 0x01U, 0xFFU, + 0x85U, 0x01U, 0x2AU}; + +constexpr std::array kWriteRequest{ + 0xA0U, 0x1BU, 0x02U, 0x01U, 0x0EU, + 0xA5U, 0x16U, + 0xA0U, 0x0FU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0xA0U, 0x03U, 0x83U, 0x01U, 0x00U}; + +constexpr std::array kWriteResponse{ + 0xA1U, 0x07U, 0x02U, 0x01U, 0x0EU, + 0xA5U, 0x02U, 0x81U, 0x00U}; + +constexpr std::array kIdentifyReject{ + 0xA4U, 0x06U, 0x80U, 0x01U, 0x15U, 0x81U, 0x01U, 0x01U}; + +constexpr std::array kMissingAttributesRequest{ + 0xA0U, 0x12U, 0x02U, 0x01U, 0x16U, + 0xA6U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x5AU, 0x5AU}; + +constexpr std::array kMissingObjectConfirmedError{ + 0xA2U, 0x0AU, 0x80U, 0x01U, 0x16U, + 0xA2U, 0x05U, 0xA0U, 0x03U, 0x87U, 0x01U, 0x02U}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto value = *static_cast(context); + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] wire::EncodeResult read_small_int32( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto value = *static_cast(context); + if (value > 0x7FU) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + destination[0] = 0x85U; + destination[1] = 0x01U; + destination[2] = value; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] mms::MmsStaticWriteResult write_boolean( + void* context, + const std::span encoded_data) noexcept { + if (context == nullptr || encoded_data.size() != 3U || + encoded_data[0] != 0x83U || encoded_data[1] != 0x01U || + (encoded_data[2] != 0x00U && encoded_data[2] != 0xFFU)) { + return {false, 7U}; + } + *static_cast(context) = encoded_data[2] != 0x00U; + return {true, 0U}; +} + +template +[[nodiscard]] wire::EncodeResult build_data_tpkt( + const std::span session_payload, + std::array& output, + std::array& scratch, + const bool eot = true) noexcept { + const auto cotp = osi::CotpSpanCodec::encode_data_into( + session_payload, scratch, eot, 0U); + if (!cotp.success()) { + return cotp; + } + return osi::TpktSpanCodec::encode_into( + std::span{scratch}.first(cotp.bytes_written), output); +} + +template +[[nodiscard]] wire::EncodeResult build_mms_tpkt( + const std::span mms_pdu, + std::array& output, + std::array& presentation, + std::array& scratch, + const bool eot = true) noexcept { + const auto p_data = osi::PresentationSpanCodec::encode_p_data_into( + mms_pdu, presentation, 3U, true); + if (!p_data.success()) { + return p_data; + } + return build_data_tpkt( + std::span{presentation}.first(p_data.bytes_written), + output, + scratch, + eot); +} + +[[nodiscard]] bool extract_cotp( + const std::span frame, + osi::CotpTpduView& cotp) noexcept { + osi::TpktFrameView tpkt; + return osi::TpktSpanCodec::try_decode_view(frame, tpkt) && + osi::CotpSpanCodec::try_decode_view(tpkt.payload, cotp); +} + +[[nodiscard]] bool extract_mms( + const std::span frame, + osi::PresentationPdvView& pdv) noexcept { + osi::CotpTpduView cotp; + osi::SessionDataTransferView session; + return extract_cotp(frame, cotp) && + cotp.kind == osi::CotpWireKind::data && + osi::SessionSpanCodec::try_decode_data_transfer_view(cotp.user_data, session) && + osi::PresentationSpanCodec::try_decode_fully_encoded_data_view( + session.presentation_payload, pdv); +} + +} // namespace + +int main() { + bool relay_state = true; + const std::uint8_t meter_value = 42U; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "R1", kBooleanType, + read_boolean, &relay_state, false, + write_boolean, &relay_state}, + mms::MmsStaticObjectEntry{ + "LD0", "M1", kInt32Type, + read_small_int32, &meter_value, false}}; + const mms::MmsStaticObjectTable table{objects}; + const mms::MmsStaticApplicationDispatcher dispatcher{table}; + mms::MmsStaticConnectionRuntime runtime{dispatcher}; + if (!table.valid() || + runtime.state() != mms::MmsStaticConnectionState::awaiting_cotp_connect) { + return 1; + } + + constexpr std::array tpdu_size{0x0AU}; + constexpr std::array source_tsap{0x00U, 0x01U}; + constexpr std::array destination_tsap{0x00U, 0x01U}; + const std::array parameters{ + osi::CotpParameterView{osi::CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + osi::CotpParameterView{osi::CotpSpanCodec::source_tsap_parameter, source_tsap}, + osi::CotpParameterView{osi::CotpSpanCodec::destination_tsap_parameter, destination_tsap}}; + + std::array request{}; + std::array response{}; + std::array workspace{}; + std::array scratch{}; + std::array presentation{}; + + const auto cr = osi::CotpSpanCodec::encode_connection_request_into( + 0x0001U, parameters, scratch); + const auto cr_tpkt = cr.success() + ? osi::TpktSpanCodec::encode_into( + std::span{scratch}.first(cr.bytes_written), request) + : wire::EncodeResult{}; + if (!cr.success() || !cr_tpkt.success()) { + return 2; + } + + auto result = runtime.process_tcp_window( + std::span{request}.first(3U), response, workspace); + if (result.status != mms::MmsStaticConnectionStatus::need_more || + result.consumed_bytes != 0U || + runtime.state() != mms::MmsStaticConnectionState::awaiting_cotp_connect) { + return 3; + } + + result = runtime.process_tcp_window( + std::span{request}.first(cr_tpkt.bytes_written), + response, + workspace); + osi::CotpTpduView cc; + if (!result.response_ready() || result.consumed_bytes != cr_tpkt.bytes_written || + runtime.state() != mms::MmsStaticConnectionState::awaiting_association || + !extract_cotp( + std::span{response}.first(result.bytes_written), cc) || + cc.kind != osi::CotpWireKind::connection_confirm || + cc.destination_reference != 0x0001U || cc.source_reference != 0x1001U) { + return 4; + } + + const auto association_tpkt = build_data_tpkt( + kAssociationRequest, request, scratch); + result = runtime.process_tcp_window( + std::span{request}.first(association_tpkt.bytes_written), + response, + workspace); + osi::CotpTpduView association_response; + if (!association_tpkt.success() || !result.response_ready() || + runtime.state() != mms::MmsStaticConnectionState::established || + runtime.mms_presentation_context_id() != 3U || + !extract_cotp( + std::span{response}.first(result.bytes_written), + association_response) || + !matches(association_response.user_data, kAssociationResponse)) { + return 5; + } + + const auto read_tpkt = build_mms_tpkt( + kReadRequest, request, presentation, scratch); + if (!read_tpkt.success()) { + return 6; + } + + std::array coalesced{}; + std::copy_n(request.begin(), read_tpkt.bytes_written, coalesced.begin()); + std::copy_n( + request.begin(), + read_tpkt.bytes_written, + coalesced.begin() + static_cast(read_tpkt.bytes_written)); + const auto coalesced_bytes = read_tpkt.bytes_written * 2U; + + result = runtime.process_tcp_window( + std::span{coalesced}.first(coalesced_bytes), + response, + workspace); + osi::PresentationPdvView pdv; + if (!result.response_ready() || result.consumed_bytes != read_tpkt.bytes_written || + result.application_service != mms::MmsWireConfirmedService::read || + !extract_mms( + std::span{response}.first(result.bytes_written), pdv) || + pdv.context_id != 3U || !matches(pdv.single_asn1_type, kReadResponse)) { + return 7; + } + + const auto second_window = std::span{coalesced} + .subspan(result.consumed_bytes, read_tpkt.bytes_written); + result = runtime.process_tcp_window(second_window, response, workspace); + if (!result.response_ready() || result.consumed_bytes != read_tpkt.bytes_written || + !extract_mms( + std::span{response}.first(result.bytes_written), pdv) || + !matches(pdv.single_asn1_type, kReadResponse)) { + return 8; + } + + for (std::uint32_t iteration = 0U; iteration < 10'000U; ++iteration) { + result = runtime.process_tcp_window( + std::span{request}.first(read_tpkt.bytes_written), + response, + workspace); + if (!result.response_ready() || + result.application_service != mms::MmsWireConfirmedService::read || + result.invoke_id != 12U) { + return 9; + } + } + + std::array identify_mms{}; + const auto identify = mms::MmsPduSpanCodec::encode_confirmed_request_into( + 21U, + static_cast(mms::MmsWireConfirmedService::identify), + false, + {}, + identify_mms); + const auto identify_tpkt = identify.success() + ? build_mms_tpkt( + std::span{identify_mms}.first(identify.bytes_written), + request, + presentation, + scratch) + : wire::EncodeResult{}; + if (!identify.success() || !identify_tpkt.success()) { + return 10; + } + result = runtime.process_tcp_window( + std::span{request}.first(identify_tpkt.bytes_written), + response, + workspace); + if (!result.response_ready() || + result.application_status != mms::MmsStaticDispatchStatus::unsupported_service || + result.application_service != mms::MmsWireConfirmedService::identify || + result.invoke_id != 21U || result.consumed_bytes != identify_tpkt.bytes_written || + runtime.state() != mms::MmsStaticConnectionState::established || + !extract_mms( + std::span{response}.first(result.bytes_written), pdv) || + !matches(pdv.single_asn1_type, kIdentifyReject)) { + return 11; + } + + const auto missing_attributes_tpkt = build_mms_tpkt( + kMissingAttributesRequest, request, presentation, scratch); + if (!missing_attributes_tpkt.success()) { + return 18; + } + result = runtime.process_tcp_window( + std::span{request}.first(missing_attributes_tpkt.bytes_written), + response, + workspace); + if (!result.response_ready() || + result.application_status != mms::MmsStaticDispatchStatus::object_not_found || + result.application_service != + mms::MmsWireConfirmedService::get_variable_access_attributes || + result.invoke_id != 22U || + result.consumed_bytes != missing_attributes_tpkt.bytes_written || + runtime.state() != mms::MmsStaticConnectionState::established || + !extract_mms( + std::span{response}.first(result.bytes_written), pdv) || + !matches(pdv.single_asn1_type, kMissingObjectConfirmedError)) { + return 19; + } + + const auto write_tpkt = build_mms_tpkt( + kWriteRequest, request, presentation, scratch); + if (!write_tpkt.success() || !relay_state) { + return 12; + } + std::array small_response{}; + result = runtime.process_tcp_window( + std::span{request}.first(write_tpkt.bytes_written), + small_response, + workspace); + if (result.status != mms::MmsStaticConnectionStatus::response_buffer_too_small || + result.consumed_bytes != 0U || result.required_response_bytes <= small_response.size() || + !relay_state || runtime.state() != mms::MmsStaticConnectionState::established) { + return 13; + } + + result = runtime.process_tcp_window( + std::span{request}.first(write_tpkt.bytes_written), + response, + workspace); + if (!result.response_ready() || relay_state || + result.application_service != mms::MmsWireConfirmedService::write || + !extract_mms( + std::span{response}.first(result.bytes_written), pdv) || + !matches(pdv.single_asn1_type, kWriteResponse)) { + return 14; + } + + const auto segmented_tpkt = build_mms_tpkt( + kReadRequest, request, presentation, scratch, false); + result = runtime.process_tcp_window( + std::span{request}.first(segmented_tpkt.bytes_written), + response, + workspace); + if (!segmented_tpkt.success() || + result.status != mms::MmsStaticConnectionStatus::protocol_violation || + runtime.state() != mms::MmsStaticConnectionState::fault) { + return 15; + } + + runtime.reset(); + if (runtime.state() != mms::MmsStaticConnectionState::awaiting_cotp_connect || + runtime.mms_presentation_context_id() != 0U) { + return 16; + } + + mms::MmsStaticConnectionRuntime wrong_order{dispatcher}; + const auto out_of_order = build_mms_tpkt( + kReadRequest, request, presentation, scratch); + result = wrong_order.process_tcp_window( + std::span{request}.first(out_of_order.bytes_written), + response, + workspace); + if (!out_of_order.success() || + result.status != mms::MmsStaticConnectionStatus::protocol_violation || + wrong_order.state() != mms::MmsStaticConnectionState::fault) { + return 17; + } + + return 0; +} \ No newline at end of file diff --git a/embedded/mms_data_set_hard_profile_smoke.cpp b/embedded/mms_data_set_hard_profile_smoke.cpp new file mode 100644 index 000000000..958e67910 --- /dev/null +++ b/embedded/mms_data_set_hard_profile_smoke.cpp @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/data_set_span.hpp" +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_dispatcher.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kAttributesRequest{ + 0xA0U, 0x19U, 0x02U, 0x01U, 0x01U, + 0xACU, 0x14U, 0xA1U, 0x12U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x0BU, 0x4CU, 0x4CU, 0x4EU, 0x30U, 0x24U, + 0x45U, 0x76U, 0x65U, 0x6EU, 0x74U, 0x73U}; +constexpr std::array kAttributesResponse{ + 0xA1U, 0x28U, 0x02U, 0x01U, 0x01U, + 0xACU, 0x23U, 0x80U, 0x01U, 0x00U, + 0xA1U, 0x1EU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x4DU, 0x31U}; +constexpr std::array kDataSetNameListRequest{ + 0xA0U, 0x11U, 0x02U, 0x01U, 0x02U, + 0xA1U, 0x0CU, + 0xA0U, 0x03U, 0x80U, 0x01U, 0x02U, + 0xA1U, 0x05U, 0x81U, 0x03U, 0x4CU, 0x44U, 0x30U}; +constexpr std::array kDataSetNameListResponse{ + 0xA1U, 0x17U, 0x02U, 0x01U, 0x02U, + 0xA1U, 0x12U, 0xA0U, 0x0DU, + 0x1AU, 0x0BU, 0x4CU, 0x4CU, 0x4EU, 0x30U, 0x24U, + 0x45U, 0x76U, 0x65U, 0x6EU, 0x74U, 0x73U, + 0x81U, 0x01U, 0x00U}; +constexpr std::array kLd0{0x4CU, 0x44U, 0x30U}; +constexpr std::array kR1{0x52U, 0x31U}; +constexpr std::array kM1{0x4DU, 0x31U}; +constexpr std::array kEvents{ + 0x4CU, 0x4CU, 0x4EU, 0x30U, 0x24U, + 0x45U, 0x76U, 0x65U, 0x6EU, 0x74U, 0x73U}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +} // namespace + +int main() { + mms::MmsNamedVariableListAttributesRequestView request; + if (!mms::MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_request( + kAttributesRequest, request) || + request.invoke_id != 1U || + request.name.kind != mms::MmsObjectNameViewKind::domain_specific || + !matches(request.name.domain, kLd0) || !matches(request.name.item, kEvents)) { + return 1; + } + + const std::array encoded_members{ + mms::MmsNamedVariableListMemberInput{"LD0", "R1"}, + mms::MmsNamedVariableListMemberInput{"LD0", "M1"}}; + std::array buffer{}; + const auto encoded = + mms::MmsDataSetSpanCodec::encode_get_named_variable_list_attributes_response_into( + 1U, false, encoded_members, buffer); + if (!encoded.success() || encoded.bytes_written != kAttributesResponse.size() || + !matches( + std::span{buffer}.first(encoded.bytes_written), + kAttributesResponse)) { + return 2; + } + + mms::MmsNamedVariableListAttributesResponseView response; + if (!mms::MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_response( + kAttributesResponse, response) || + response.invoke_id != 1U || response.mms_deletable || response.member_count != 2U) { + return 3; + } + mms::MmsObjectNameView member; + if (!response.try_member(0U, member) || + !matches(member.domain, kLd0) || !matches(member.item, kR1) || + !response.try_member(1U, member) || + !matches(member.domain, kLd0) || !matches(member.item, kM1) || + response.try_member(2U, member)) { + return 4; + } + + const bool relay_state = true; + const bool meter_state = false; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "R1", kBooleanType, read_boolean, &relay_state, false}, + mms::MmsStaticObjectEntry{ + "LD0", "M1", kBooleanType, read_boolean, &meter_state, false}}; + const mms::MmsStaticObjectTable object_table{objects}; + + const std::array event_members{ + mms::MmsStaticDataSetMember{"LD0", "R1"}, + mms::MmsStaticDataSetMember{"LD0", "M1"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", event_members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + if (!data_set_table.valid() || !data_set_table.valid_against(object_table)) { + return 5; + } + + const mms::MmsStaticApplicationDispatcher dispatcher{object_table, data_set_table}; + std::array workspace{}; + auto dispatched = dispatcher.dispatch(kAttributesRequest, buffer, workspace); + if (!dispatched.success() || + dispatched.service != mms::MmsWireConfirmedService::get_named_variable_list_attributes || + dispatched.invoke_id != 1U || dispatched.bytes_written != kAttributesResponse.size() || + !matches( + std::span{buffer}.first(dispatched.bytes_written), + kAttributesResponse)) { + return 6; + } + + dispatched = dispatcher.dispatch(kDataSetNameListRequest, buffer, workspace); + if (!dispatched.success() || + dispatched.service != mms::MmsWireConfirmedService::get_name_list || + dispatched.bytes_written != kDataSetNameListResponse.size() || + !matches( + std::span{buffer}.first(dispatched.bytes_written), + kDataSetNameListResponse)) { + return 7; + } + + mms::MmsGetNameListResponseView names; + std::span identifier; + if (!mms::MmsServiceSpanCodec::try_decode_get_name_list_response( + std::span{buffer}.first(dispatched.bytes_written), names) || + names.identifier_count != 1U || names.more_follows || + !names.try_identifier(0U, identifier) || !matches(identifier, kEvents)) { + return 8; + } + + std::array tiny{}; + dispatched = dispatcher.dispatch(kAttributesRequest, tiny, workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::response_buffer_too_small || + dispatched.required_bytes != kAttributesResponse.size()) { + return 9; + } + + auto missing_request = kAttributesRequest; + missing_request[26] = 0x58U; + dispatched = dispatcher.dispatch(missing_request, buffer, workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::object_not_found) { + return 10; + } + + const std::array invalid_members{ + mms::MmsStaticDataSetMember{"LD0", "X1"}}; + const std::array invalid_data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Broken", invalid_members, false}}; + const mms::MmsStaticDataSetTable invalid_table{invalid_data_sets}; + if (!invalid_table.valid() || invalid_table.valid_against(object_table)) { + return 11; + } + + for (std::uint32_t iteration = 0U; iteration < 50'000U; ++iteration) { + const auto invoke = iteration & 0x7FFFU; + const auto repeated = + mms::MmsDataSetSpanCodec::encode_get_named_variable_list_attributes_response_into( + invoke, (iteration & 1U) != 0U, encoded_members, buffer); + mms::MmsNamedVariableListAttributesResponseView decoded; + if (!repeated.success() || + !mms::MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_response( + std::span{buffer}.first(repeated.bytes_written), decoded) || + decoded.invoke_id != invoke || decoded.member_count != 2U || + decoded.mms_deletable != ((iteration & 1U) != 0U)) { + return 12; + } + } + + const std::array invalid_name{ + mms::MmsNamedVariableListMemberInput{"LD0", std::string_view{"A\0B", 3U}}}; + if (mms::MmsDataSetSpanCodec::encode_get_named_variable_list_attributes_response_into( + 1U, false, invalid_name, buffer).success()) { + return 13; + } + + return 0; +} diff --git a/embedded/mms_dispatcher_hard_profile_smoke.cpp b/embedded/mms_dispatcher_hard_profile_smoke.cpp new file mode 100644 index 000000000..04b902339 --- /dev/null +++ b/embedded/mms_dispatcher_hard_profile_smoke.cpp @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_dispatcher.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kInt32Type{0x85U, 0x01U, 0x20U}; + +constexpr std::array kDomainListRequest{ + 0xA0U, 0x0EU, 0x02U, 0x01U, 0x07U, + 0xA1U, 0x09U, + 0xA0U, 0x03U, 0x80U, 0x01U, 0x09U, + 0xA1U, 0x02U, 0x80U, 0x00U}; + +constexpr std::array kDomainContinueRequest{ + 0xA0U, 0x13U, 0x02U, 0x01U, 0x08U, + 0xA1U, 0x0EU, + 0xA0U, 0x03U, 0x80U, 0x01U, 0x09U, + 0xA1U, 0x02U, 0x80U, 0x00U, + 0x82U, 0x03U, 0x4CU, 0x44U, 0x30U}; + +constexpr std::array kNamedVariableDirectoryRequest{ + 0xA0U, 0x11U, 0x02U, 0x01U, 0x16U, + 0xA1U, 0x0CU, + 0xA0U, 0x03U, 0x80U, 0x01U, 0x00U, + 0xA1U, 0x05U, 0x81U, 0x03U, 0x4CU, 0x44U, 0x48U}; + +constexpr std::array kDomainFirstResponse{ + 0xA1U, 0x0FU, 0x02U, 0x01U, 0x07U, + 0xA1U, 0x0AU, 0xA0U, 0x05U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x81U, 0x01U, 0xFFU}; + +constexpr std::array kDomainSecondResponse{ + 0xA1U, 0x0FU, 0x02U, 0x01U, 0x08U, + 0xA1U, 0x0AU, 0xA0U, 0x05U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x31U, + 0x81U, 0x01U, 0x00U}; + +constexpr std::array kAttributesRequest{ + 0xA0U, 0x12U, 0x02U, 0x01U, 0x0BU, + 0xA6U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U}; + +constexpr std::array kAttributesResponse{ + 0xA1U, 0x0CU, 0x02U, 0x01U, 0x0BU, + 0xA6U, 0x07U, 0x80U, 0x01U, 0x00U, + 0xA2U, 0x02U, 0x83U, 0x00U}; + +constexpr std::array kReadRequest{ + 0xA0U, 0x27U, 0x02U, 0x01U, 0x0CU, + 0xA4U, 0x22U, 0xA1U, 0x20U, 0xA0U, 0x1EU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x4DU, 0x31U}; + +constexpr std::array kReadWithSpecificationRequest{ + 0xA0U, 0x2AU, 0x02U, 0x01U, 0x0DU, + 0xA4U, 0x25U, 0x80U, 0x01U, 0xFFU, + 0xA1U, 0x20U, 0xA0U, 0x1EU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x4DU, 0x31U}; + +constexpr std::array kReadResponse{ + 0xA1U, 0x0DU, 0x02U, 0x01U, 0x0CU, + 0xA4U, 0x08U, 0xA1U, 0x06U, + 0x83U, 0x01U, 0xFFU, + 0x85U, 0x01U, 0x2AU}; + +constexpr std::array kWriteRequest{ + 0xA0U, 0x1BU, 0x02U, 0x01U, 0x0EU, + 0xA5U, 0x16U, + 0xA0U, 0x0FU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0xA0U, 0x03U, 0x83U, 0x01U, 0x00U}; + +constexpr std::array kWriteSuccessResponse{ + 0xA1U, 0x07U, 0x02U, 0x01U, 0x0EU, + 0xA5U, 0x02U, 0x81U, 0x00U}; + +constexpr std::array kWriteDeniedResponse{ + 0xA1U, 0x08U, 0x02U, 0x01U, 0x0FU, + 0xA5U, 0x03U, 0x80U, 0x01U, 0x03U}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] bool identifier_equals( + const std::span identifier, + const std::string_view expected) noexcept { + if (identifier.size() != expected.size()) { + return false; + } + for (std::size_t index = 0U; index < identifier.size(); ++index) { + if (identifier[index] != static_cast( + static_cast(expected[index]))) { + return false; + } + } + return true; +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto value = *static_cast(context); + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] wire::EncodeResult read_small_int32( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr || destination.size() < required) { + return context == nullptr + ? wire::EncodeResult{wire::EncodeStatus::value_out_of_range, 0U, required} + : wire::EncodeResult{wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto value = *static_cast(context); + if (value > 0x7FU) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + destination[0] = 0x85U; + destination[1] = 0x01U; + destination[2] = value; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] mms::MmsStaticWriteResult write_boolean( + void* context, + const std::span encoded_data) noexcept { + if (context == nullptr || encoded_data.size() != 3U || + encoded_data[0] != 0x83U || encoded_data[1] != 0x01U || + (encoded_data[2] != 0x00U && encoded_data[2] != 0xFFU)) { + return {false, 7U}; + } + *static_cast(context) = encoded_data[2] != 0x00U; + return {true, 0U}; +} + +} // namespace + +int main() { + bool relay_state = true; + const std::uint8_t meter_value = 42U; + const bool remote_state = false; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "R1", kBooleanType, + read_boolean, &relay_state, false, + write_boolean, &relay_state}, + mms::MmsStaticObjectEntry{ + "LD0", "M1", kInt32Type, + read_small_int32, &meter_value, false}, + mms::MmsStaticObjectEntry{ + "LD1", "X1", kBooleanType, + read_boolean, &remote_state, false}}; + const mms::MmsStaticObjectTable table{objects}; + if (!table.valid()) { + return 1; + } + + const mms::MmsStaticDispatchPolicy policy{ + 1U, + 1U, + 10U, + 3U, + 10U}; + const mms::MmsStaticApplicationDispatcher dispatcher{table, policy}; + std::array response{}; + std::array workspace{}; + + auto dispatched = dispatcher.dispatch(kDomainListRequest, response, workspace); + if (!dispatched.success() || + dispatched.service != mms::MmsWireConfirmedService::get_name_list || + dispatched.invoke_id != 7U || + dispatched.bytes_written != kDomainFirstResponse.size() || + !matches( + std::span{response}.first(dispatched.bytes_written), + kDomainFirstResponse)) { + return 2; + } + + dispatched = dispatcher.dispatch(kDomainContinueRequest, response, workspace); + if (!dispatched.success() || dispatched.invoke_id != 8U || + dispatched.bytes_written != kDomainSecondResponse.size() || + !matches( + std::span{response}.first(dispatched.bytes_written), + kDomainSecondResponse)) { + return 3; + } + + mms::MmsConfirmedPduView attributes_view; + if (!mms::MmsPduSpanCodec::try_decode_confirmed_request_view( + kAttributesRequest, attributes_view)) { + return 4; + } + dispatched = dispatcher.dispatch(attributes_view, response, workspace); + if (!dispatched.success() || + dispatched.service != mms::MmsWireConfirmedService::get_variable_access_attributes || + dispatched.bytes_written != kAttributesResponse.size() || + !matches( + std::span{response}.first(dispatched.bytes_written), + kAttributesResponse)) { + return 5; + } + + dispatched = dispatcher.dispatch(kReadRequest, response, workspace); + if (!dispatched.success() || + dispatched.service != mms::MmsWireConfirmedService::read || + dispatched.bytes_written != kReadResponse.size() || + !matches( + std::span{response}.first(dispatched.bytes_written), + kReadResponse)) { + return 6; + } + + dispatched = dispatcher.dispatch( + kReadWithSpecificationRequest, response, workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::unsupported_request || + dispatched.service != mms::MmsWireConfirmedService::read || + dispatched.invoke_id != 13U) { + return 30; + } + + std::array tiny_workspace{}; + dispatched = dispatcher.dispatch(kReadRequest, response, tiny_workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::workspace_too_small || + dispatched.required_bytes < 3U) { + return 7; + } + + auto missing_read = kReadRequest; + missing_read[39] = 0x5AU; + dispatched = dispatcher.dispatch(missing_read, response, workspace); + if (!dispatched.success()) { + return 8; + } + mms::MmsReadResponseView read_response; + mms::MmsReadAccessResultView read_result; + if (!mms::MmsServiceSpanCodec::try_decode_read_response( + std::span{response}.first(dispatched.bytes_written), + read_response) || + read_response.result_count != 2U || + !read_response.try_result(0U, read_result) || !read_result.success || + !read_response.try_result(1U, read_result) || read_result.success || + read_result.failure_code != 10U) { + return 9; + } + + dispatched = dispatcher.dispatch(kWriteRequest, response, workspace); + if (!dispatched.success() || relay_state || + dispatched.service != mms::MmsWireConfirmedService::write || + dispatched.bytes_written != kWriteSuccessResponse.size() || + !matches( + std::span{response}.first(dispatched.bytes_written), + kWriteSuccessResponse)) { + return 10; + } + + auto read_only_write = kWriteRequest; + read_only_write[4] = 0x0FU; + read_only_write[22] = 0x4DU; + dispatched = dispatcher.dispatch(read_only_write, response, workspace); + if (!dispatched.success() || + dispatched.bytes_written != kWriteDeniedResponse.size() || + !matches( + std::span{response}.first(dispatched.bytes_written), + kWriteDeniedResponse) || + meter_value != 42U) { + return 11; + } + + std::array tiny_response{}; + dispatched = dispatcher.dispatch(kDomainListRequest, tiny_response, workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::response_buffer_too_small || + dispatched.required_bytes != kDomainFirstResponse.size()) { + return 12; + } + + auto unknown_continuation = kDomainContinueRequest; + unknown_continuation[18] = 0x5AU; + unknown_continuation[19] = 0x5AU; + unknown_continuation[20] = 0x5AU; + dispatched = dispatcher.dispatch(unknown_continuation, response, workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::object_not_found) { + return 13; + } + + std::array identify_request{}; + const auto identify_encoded = mms::MmsPduSpanCodec::encode_confirmed_request_into( + 21U, + static_cast(mms::MmsWireConfirmedService::identify), + false, + {}, + identify_request); + if (!identify_encoded.success()) { + return 14; + } + dispatched = dispatcher.dispatch( + std::span{identify_request}.first(identify_encoded.bytes_written), + response, + workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::unsupported_service || + dispatched.invoke_id != 21U) { + return 15; + } + + constexpr std::array malformed{0x00U}; + dispatched = dispatcher.dispatch(malformed, response, workspace); + if (dispatched.status != mms::MmsStaticDispatchStatus::malformed_request || + dispatched.service != mms::MmsWireConfirmedService::unknown) { + return 16; + } + + // ARIEC61850 IED simulator parity: when a root Logical Node object exists, + // GetNameList(NamedVariable) advertises the root and leaves its flattened + // FC/DO/DA aliases for Read/Write lookup and TypeSpecification traversal. + // A flat-only generic MMS item remains discoverable for compatibility. + bool hierarchy_value = true; + const std::array hierarchy_objects{ + mms::MmsStaticObjectEntry{ + "LDH", "LLN0", kBooleanType, + read_boolean, &hierarchy_value, false}, + mms::MmsStaticObjectEntry{ + "LDH", "LLN0$ST$Mod$stVal", kBooleanType, + read_boolean, &hierarchy_value, false}, + mms::MmsStaticObjectEntry{ + "LDH", "GGIO1", kBooleanType, + read_boolean, &hierarchy_value, false}, + mms::MmsStaticObjectEntry{ + "LDH", "GGIO1$ST$Ind1$stVal", kBooleanType, + read_boolean, &hierarchy_value, false}, + mms::MmsStaticObjectEntry{ + "LDH", "Orphan$ST$stVal", kBooleanType, + read_boolean, &hierarchy_value, false}}; + const mms::MmsStaticObjectTable hierarchy_table{hierarchy_objects}; + const mms::MmsStaticDispatchPolicy hierarchy_policy{ + 8U, + 1U, + 10U, + 3U, + 10U}; + const mms::MmsStaticApplicationDispatcher hierarchy_dispatcher{ + hierarchy_table, + hierarchy_policy}; + if (!hierarchy_table.valid()) { + return 17; + } + + dispatched = hierarchy_dispatcher.dispatch( + kNamedVariableDirectoryRequest, + response, + workspace); + mms::MmsGetNameListResponseView hierarchy_directory; + if (!dispatched.success() || + !mms::MmsServiceSpanCodec::try_decode_get_name_list_response( + std::span{response}.first(dispatched.bytes_written), + hierarchy_directory) || + hierarchy_directory.invoke_id != 22U || + hierarchy_directory.identifier_count != 3U || + hierarchy_directory.more_follows) { + return 18; + } + + std::span identifier; + if (!hierarchy_directory.try_identifier(0U, identifier) || + !identifier_equals(identifier, "LLN0") || + !hierarchy_directory.try_identifier(1U, identifier) || + !identifier_equals(identifier, "GGIO1") || + !hierarchy_directory.try_identifier(2U, identifier) || + !identifier_equals(identifier, "Orphan$ST$stVal")) { + return 19; + } + + auto dual_directory_policy = hierarchy_policy; + dual_directory_policy.advertise_flattened_child_aliases = true; + const mms::MmsStaticApplicationDispatcher dual_directory_dispatcher{ + hierarchy_table, + dual_directory_policy}; + dispatched = dual_directory_dispatcher.dispatch( + kNamedVariableDirectoryRequest, + response, + workspace); + mms::MmsGetNameListResponseView dual_directory; + if (!dispatched.success() || + !mms::MmsServiceSpanCodec::try_decode_get_name_list_response( + std::span{response}.first(dispatched.bytes_written), + dual_directory) || + dual_directory.identifier_count != 5U || + dual_directory.more_follows) { + return 20; + } + constexpr std::array dual_names{ + "LLN0", + "LLN0$ST$Mod$stVal", + "GGIO1", + "GGIO1$ST$Ind1$stVal", + "Orphan$ST$stVal"}; + for (std::size_t index = 0U; index < dual_names.size(); ++index) { + if (!dual_directory.try_identifier(index, identifier) || + !identifier_equals(identifier, dual_names[index])) { + return 21; + } + } + + for (std::uint32_t iteration = 0U; iteration < 20'000U; ++iteration) { + relay_state = true; + const auto read = dispatcher.dispatch(kReadRequest, response, workspace); + const auto write = dispatcher.dispatch(kWriteRequest, response, workspace); + if (!read.success() || !write.success() || relay_state) { + return 22; + } + } + + return 0; +} diff --git a/embedded/mms_get_name_list_hard_profile_smoke.cpp b/embedded/mms_get_name_list_hard_profile_smoke.cpp new file mode 100644 index 000000000..61c7c5e7d --- /dev/null +++ b/embedded/mms_get_name_list_hard_profile_smoke.cpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kDomainListRequest{ + 0xA0U, 0x0EU, 0x02U, 0x01U, 0x07U, + 0xA1U, 0x09U, + 0xA0U, 0x03U, 0x80U, 0x01U, 0x09U, + 0xA1U, 0x02U, 0x80U, 0x00U}; + +constexpr std::array kDomainScopedRequest{ + 0xA0U, 0x14U, 0x02U, 0x01U, 0x08U, + 0xA1U, 0x0FU, + 0xA0U, 0x03U, 0x80U, 0x01U, 0x00U, + 0xA1U, 0x05U, 0x81U, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x82U, 0x01U, 0x58U}; + +constexpr std::array kResponse{ + 0xA1U, 0x14U, 0x02U, 0x01U, 0x07U, + 0xA1U, 0x0FU, + 0xA0U, 0x0AU, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x31U, + 0x81U, 0x01U, 0x00U}; + +constexpr std::array kLd0{0x4CU, 0x44U, 0x30U}; +constexpr std::array kLd1{0x4CU, 0x44U, 0x31U}; +constexpr std::array kContinuation{0x58U}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +} // namespace + +int main() { + mms::MmsGetNameListRequestView request; + if (!mms::MmsServiceSpanCodec::try_decode_get_name_list_request( + kDomainListRequest, request) || + request.invoke_id != 7U || + request.object_class != mms::MmsNameListObjectClass::domain || + request.scope != mms::MmsNameScopeKind::vmd_specific || + !request.domain_id.empty() || !request.continue_after.empty()) { + return 1; + } + + if (!mms::MmsServiceSpanCodec::try_decode_get_name_list_request( + kDomainScopedRequest, request) || + request.invoke_id != 8U || + request.object_class != mms::MmsNameListObjectClass::named_variable || + request.scope != mms::MmsNameScopeKind::domain_specific || + !matches(request.domain_id, kLd0) || + !matches(request.continue_after, kContinuation)) { + return 2; + } + + const std::array names{"LD0", "LD1"}; + std::array buffer{}; + const auto encoded = mms::MmsServiceSpanCodec::encode_get_name_list_response_into( + 7U, names, false, buffer); + if (!encoded.success() || encoded.bytes_written != kResponse.size() || + !matches( + std::span{buffer}.first(encoded.bytes_written), + kResponse)) { + return 3; + } + + mms::MmsGetNameListResponseView response; + if (!mms::MmsServiceSpanCodec::try_decode_get_name_list_response( + kResponse, response) || + response.invoke_id != 7U || response.identifier_count != 2U || + response.more_follows) { + return 4; + } + + std::span identifier; + if (!response.try_identifier(0U, identifier) || !matches(identifier, kLd0) || + !response.try_identifier(1U, identifier) || !matches(identifier, kLd1) || + response.try_identifier(2U, identifier)) { + return 5; + } + + // Post-association application flow: response PDU -> Presentation P-DATA -> + // zero-copy Presentation view -> zero-copy GetNameList response view. + std::array p_data{}; + const auto p_data_result = osi::PresentationSpanCodec::encode_p_data_into( + kResponse, p_data, 3U, true); + osi::PresentationPdvView pdv; + if (!p_data_result.success() || + !osi::PresentationSpanCodec::try_decode_p_data_view( + std::span{p_data}.first(p_data_result.bytes_written), + pdv) || + pdv.context_id != 3U || + !mms::MmsServiceSpanCodec::try_decode_get_name_list_response( + pdv.single_asn1_type, response) || + response.identifier_count != 2U) { + return 6; + } + + // Empty lists are valid MMS responses; pagination is expressed separately + // by moreFollows. + const std::span empty_names; + const auto empty = mms::MmsServiceSpanCodec::encode_get_name_list_response_into( + 9U, empty_names, true, buffer); + if (!empty.success() || + !mms::MmsServiceSpanCodec::try_decode_get_name_list_response( + std::span{buffer}.first(empty.bytes_written), + response) || + response.identifier_count != 0U || !response.more_follows) { + return 7; + } + + for (std::uint32_t iteration = 0U; iteration < 50'000U; ++iteration) { + const auto invoke = iteration & 0x7FFFU; + const auto repeated = mms::MmsServiceSpanCodec::encode_get_name_list_response_into( + invoke, names, (iteration & 1U) != 0U, buffer); + mms::MmsGetNameListResponseView decoded; + if (!repeated.success() || + !mms::MmsServiceSpanCodec::try_decode_get_name_list_response( + std::span{buffer}.first(repeated.bytes_written), + decoded) || + decoded.invoke_id != invoke || decoded.identifier_count != 2U || + decoded.more_follows != ((iteration & 1U) != 0U)) { + return 8; + } + } + + constexpr std::array malformed_scope{ + 0xA0U, 0x0EU, 0x02U, 0x01U, 0x07U, + 0xA1U, 0x09U, + 0xA0U, 0x03U, 0x80U, 0x01U, 0x09U, + 0xA1U, 0x02U, 0x80U, 0x01U}; + if (mms::MmsServiceSpanCodec::try_decode_get_name_list_request( + malformed_scope, request)) { + return 9; + } + + const std::array invalid_name{ + std::string_view{"A\0B", 3U}}; + if (mms::MmsServiceSpanCodec::encode_get_name_list_response_into( + 1U, invalid_name, false, buffer).success()) { + return 10; + } + + return 0; +} diff --git a/embedded/mms_negotiated_limits_hard_profile_smoke.cpp b/embedded/mms_negotiated_limits_hard_profile_smoke.cpp new file mode 100644 index 000000000..c3ba8b7ed --- /dev/null +++ b/embedded/mms_negotiated_limits_hard_profile_smoke.cpp @@ -0,0 +1,462 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/connection_runtime.hpp" +#include "ariec61850/mms/static_brcb_connection.hpp" +#include "ariec61850/mms/static_brcb_control.hpp" +#include "ariec61850/mms/static_brcb_runtime.hpp" +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_object_table.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kOctet80Type{0x89U, 0x01U, 0x50U}; +constexpr std::array kAssociationRequest{ + 0x0DU,0xB6U,0x05U,0x06U,0x13U,0x01U,0x00U,0x16U,0x01U,0x02U,0x14U,0x02U, + 0x00U,0x02U,0x33U,0x02U,0x00U,0x01U,0x34U,0x02U,0x00U,0x01U,0xC1U,0xA0U, + 0x31U,0x81U,0x9DU,0xA0U,0x03U,0x80U,0x01U,0x01U,0xA2U,0x81U,0x95U,0x81U, + 0x04U,0x00U,0x00U,0x00U,0x01U,0x82U,0x04U,0x00U,0x00U,0x00U,0x01U,0xA4U, + 0x23U,0x30U,0x0FU,0x02U,0x01U,0x01U,0x06U,0x04U,0x52U,0x01U,0x00U,0x01U, + 0x30U,0x04U,0x06U,0x02U,0x51U,0x01U,0x30U,0x10U,0x02U,0x01U,0x03U,0x06U, + 0x05U,0x28U,0xCAU,0x22U,0x02U,0x01U,0x30U,0x04U,0x06U,0x02U,0x51U,0x01U, + 0x61U,0x62U,0x30U,0x60U,0x02U,0x01U,0x01U,0xA0U,0x5BU,0x60U,0x59U,0xA1U, + 0x07U,0x06U,0x05U,0x28U,0xCAU,0x22U,0x02U,0x03U,0xA2U,0x07U,0x06U,0x05U, + 0x29U,0x01U,0x87U,0x67U,0x01U,0xA3U,0x03U,0x02U,0x01U,0x0CU,0xA6U,0x06U, + 0x06U,0x04U,0x29U,0x01U,0x87U,0x67U,0xA7U,0x03U,0x02U,0x01U,0x0CU,0xBEU, + 0x33U,0x28U,0x31U,0x06U,0x02U,0x51U,0x01U,0x02U,0x01U,0x03U,0xA0U,0x28U, + 0xA8U,0x26U,0x80U,0x03U,0x00U,0xFDU,0xE8U,0x81U,0x01U,0x0AU,0x82U,0x01U, + 0x0AU,0x83U,0x01U,0x05U,0xA4U,0x16U,0x80U,0x01U,0x01U,0x81U,0x03U,0x05U, + 0xF1U,0x00U,0x82U,0x0CU,0x03U,0xEEU,0x1CU,0x00U,0x00U,0x04U,0x08U,0x00U, + 0x00U,0x79U,0xEFU,0x18U}; + +constexpr std::array kReadRequest{ + 0xA0U,0x27U,0x02U,0x01U,0x0CU, + 0xA4U,0x22U,0xA1U,0x20U,0xA0U,0x1EU, + 0x30U,0x0DU,0xA0U,0x0BU,0xA1U,0x09U, + 0x1AU,0x03U,0x4CU,0x44U,0x30U, + 0x1AU,0x02U,0x52U,0x31U, + 0x30U,0x0DU,0xA0U,0x0BU,0xA1U,0x09U, + 0x1AU,0x03U,0x4CU,0x44U,0x30U, + 0x1AU,0x02U,0x4DU,0x31U}; + +[[nodiscard]] wire::EncodeResult read_large_octets( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t payload_bytes = 80U; + constexpr std::size_t required = payload_bytes + 2U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto& payload = *static_cast*>(context); + destination[0] = 0x89U; + destination[1] = static_cast(payload_bytes); + for (std::size_t index = 0U; index < payload.size(); ++index) { + destination[index + 2U] = payload[index]; + } + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] mms::MmsStaticWriteResult write_boolean( + void* context, + const std::span encoded_data) noexcept { + if (context == nullptr || encoded_data.size() != 3U || + encoded_data[0] != 0x83U || encoded_data[1] != 0x01U) { + return {false, 10U}; + } + *static_cast(context) = encoded_data[2] != 0U; + return {true, 0U}; +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, 3U}; + } + if (destination.size() < 3U) { + return {wire::EncodeStatus::buffer_too_small, 0U, 3U}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, 3U, 3U}; +} + +[[nodiscard]] bool set_maximum_mms_pdu( + std::array& association, + const std::uint32_t value) noexcept { + if (value > 0x00FF'FFFFU) { + return false; + } + constexpr std::array marker{0x80U,0x03U,0x00U,0xFDU,0xE8U}; + for (std::size_t index = 0U; index + marker.size() <= association.size(); ++index) { + bool match = true; + for (std::size_t marker_index = 0U; marker_index < marker.size(); ++marker_index) { + if (association[index + marker_index] != marker[marker_index]) { + match = false; + break; + } + } + if (!match) { + continue; + } + association[index + 2U] = static_cast((value >> 16U) & 0xFFU); + association[index + 3U] = static_cast((value >> 8U) & 0xFFU); + association[index + 4U] = static_cast(value & 0xFFU); + return true; + } + return false; +} + +[[nodiscard]] bool set_initiate_negotiation( + std::array& association, + const std::uint8_t outstanding_calling, + const std::uint8_t outstanding_called, + const std::uint8_t nesting_level) noexcept { + bool calling_set = false; + bool called_set = false; + bool nesting_set = false; + for (std::size_t index = 0U; index + 3U <= association.size(); ++index) { + if (association[index + 1U] != 0x01U) { + continue; + } + if (!calling_set && association[index] == 0x81U) { + association[index + 2U] = outstanding_calling; + calling_set = true; + } else if (!called_set && association[index] == 0x82U) { + association[index + 2U] = outstanding_called; + called_set = true; + } else if (!nesting_set && association[index] == 0x83U) { + association[index + 2U] = nesting_level; + nesting_set = true; + } + } + return calling_set && called_set && nesting_set; +} + +[[nodiscard]] bool decode_initiate_response( + const std::span response, + mms::MmsInitiateView& initiate) noexcept { + for (std::size_t index = 0U; index + 2U <= response.size(); ++index) { + if (response[index] != 0xA9U) { + continue; + } + const auto content = static_cast(response[index + 1U]); + if ((response[index + 1U] & 0x80U) != 0U || + content > response.size() - index - 2U) { + continue; + } + if (mms::MmsPduSpanCodec::try_decode_initiate_response_view( + response.subspan(index, content + 2U), initiate)) { + return true; + } + } + return false; +} + +[[nodiscard]] wire::EncodeResult build_ten_variable_write( + std::array& output) noexcept { + constexpr std::array definition{ + 0x30U,0x0DU,0xA0U,0x0BU,0xA1U,0x09U, + 0x1AU,0x03U,0x4CU,0x44U,0x30U, + 0x1AU,0x02U,0x57U,0x31U}; + std::array service{}; + std::size_t offset = 0U; + service[offset++] = 0xA0U; + service[offset++] = 0x81U; + service[offset++] = 0x96U; + for (std::size_t index = 0U; index < 10U; ++index) { + for (const auto byte : definition) { + service[offset++] = byte; + } + } + service[offset++] = 0xA0U; + service[offset++] = 0x1EU; + for (std::size_t index = 0U; index < 10U; ++index) { + service[offset++] = 0x83U; + service[offset++] = 0x01U; + service[offset++] = 0xFFU; + } + if (offset != service.size()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + return mms::MmsPduSpanCodec::encode_confirmed_request_into( + 20U, 5, true, service, output); +} + +template +[[nodiscard]] wire::EncodeResult build_data_tpkt( + const std::span payload, + std::array& output, + std::array& scratch) noexcept { + const auto cotp = osi::CotpSpanCodec::encode_data_into(payload, scratch); + if (!cotp.success()) { + return cotp; + } + return osi::TpktSpanCodec::encode_into( + std::span{scratch}.first(cotp.bytes_written), output); +} + +template +[[nodiscard]] wire::EncodeResult build_mms_tpkt( + const std::span mms_pdu, + std::array& output, + std::array& presentation, + std::array& scratch) noexcept { + const auto p_data = osi::PresentationSpanCodec::encode_p_data_into( + mms_pdu, presentation, 3U, true); + if (!p_data.success()) { + return p_data; + } + return build_data_tpkt( + std::span{presentation}.first(p_data.bytes_written), + output, + scratch); +} + +[[nodiscard]] bool connect_cotp( + mms::MmsStaticConnectionRuntime& runtime, + const std::uint8_t tpdu_size_code, + std::span request, + std::span response, + std::span workspace, + std::span scratch) noexcept { + const std::array tpdu_size{tpdu_size_code}; + constexpr std::array source_tsap{0x00U,0x01U}; + constexpr std::array destination_tsap{0x00U,0x01U}; + const std::array parameters{ + osi::CotpParameterView{osi::CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + osi::CotpParameterView{osi::CotpSpanCodec::source_tsap_parameter, source_tsap}, + osi::CotpParameterView{osi::CotpSpanCodec::destination_tsap_parameter, destination_tsap}}; + + const auto cr = osi::CotpSpanCodec::encode_connection_request_into( + 0x0001U, parameters, scratch); + if (!cr.success()) { + return false; + } + const auto tpkt = osi::TpktSpanCodec::encode_into( + scratch.first(cr.bytes_written), request); + if (!tpkt.success()) { + return false; + } + const auto result = runtime.process_tcp_window( + request.first(tpkt.bytes_written), response, workspace); + std::size_t expected{}; + return osi::CotpSpanCodec::try_tpdu_size_bytes(tpdu_size_code, expected) && + result.response_ready() && + runtime.state() == mms::MmsStaticConnectionState::awaiting_association && + runtime.negotiated_tpdu_size_bytes() == expected; +} + +[[nodiscard]] mms::MmsStaticConnectionResult associate( + mms::MmsStaticConnectionRuntime& runtime, + const std::uint32_t maximum_mms_pdu, + std::span request, + std::span response, + std::span workspace, + std::span scratch, + const std::uint8_t outstanding_calling = 10U, + const std::uint8_t outstanding_called = 10U, + const std::uint8_t nesting_level = 5U) noexcept { + auto association = kAssociationRequest; + if (!set_maximum_mms_pdu(association, maximum_mms_pdu) || + !set_initiate_negotiation( + association, + outstanding_calling, + outstanding_called, + nesting_level)) { + return {}; + } + const auto cotp = osi::CotpSpanCodec::encode_data_into(association, scratch); + if (!cotp.success()) { + return {}; + } + const auto tpkt = osi::TpktSpanCodec::encode_into( + scratch.first(cotp.bytes_written), request); + if (!tpkt.success()) { + return {}; + } + return runtime.process_tcp_window( + request.first(tpkt.bytes_written), response, workspace); +} + +} // namespace + +int main() { + std::array large_value{}; + for (std::size_t index = 0U; index < large_value.size(); ++index) { + large_value[index] = static_cast(index); + } + bool write_state = false; + constexpr std::array boolean_type{0x83U,0x00U}; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "R1", kOctet80Type, read_large_octets, &large_value, false}, + mms::MmsStaticObjectEntry{ + "LD0", "M1", kOctet80Type, read_large_octets, &large_value, false}, + mms::MmsStaticObjectEntry{ + "LD0", "W1", boolean_type, read_boolean, &write_state, false, + write_boolean, &write_state}}; + const mms::MmsStaticObjectTable object_table{objects}; + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "R1"}}; + const std::array data_sets{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + mms::MmsStaticDispatchPolicy dispatch_policy; + dispatch_policy.maximum_write_variables = 10U; + const mms::MmsStaticApplicationDispatcher dispatcher{ + object_table, data_set_table, dispatch_policy}; + if (!object_table.valid() || !data_set_table.valid_against(object_table)) { + return 1; + } + + std::array request{}; + std::array response{}; + std::array workspace{}; + std::array scratch{}; + std::array presentation{}; + + // A 128-byte TPDU cannot carry the fixed association-accept TSDU as one DT + // TPDU. Until outbound COTP segmentation is implemented, reject it rather + // than violate the negotiated limit. + mms::MmsStaticConnectionRuntime tiny_tpdu{dispatcher}; + if (!connect_cotp(tiny_tpdu, 0x07U, request, response, workspace, scratch)) { + return 2; + } + const auto tiny_association = associate( + tiny_tpdu, 65'000U, request, response, workspace, scratch); + if (tiny_association.status != mms::MmsStaticConnectionStatus::peer_limit_exceeded || + tiny_association.bytes_written != 0U || + tiny_tpdu.negotiated_tpdu_size_bytes() != 128U || + tiny_tpdu.state() != mms::MmsStaticConnectionState::awaiting_association) { + return 3; + } + + mms::MmsStaticConnectionPolicy policy; + policy.association_id = 101U; + policy.owner[0] = 0xAAU; + policy.owner[1] = 0x01U; + policy.owner_size = 2U; + mms::MmsStaticConnectionRuntime runtime{dispatcher, policy}; + if (!connect_cotp(runtime, 0x09U, request, response, workspace, scratch)) { + return 4; + } + const auto association = associate( + runtime, 64U, request, response, workspace, scratch, 2U, 3U, 1U); + mms::MmsInitiateView negotiated; + if (!association.response_ready() || + runtime.state() != mms::MmsStaticConnectionState::established || + runtime.negotiated_tpdu_size_bytes() != 512U || + runtime.negotiated_mms_pdu_size() != 64U || + !decode_initiate_response( + std::span{response}.first(association.bytes_written), + negotiated) || + negotiated.maximum_mms_pdu_size != 64U || + negotiated.maximum_outstanding_calling != 2U || + negotiated.maximum_outstanding_called != 3U || + negotiated.data_structure_nesting_level != 1U) { + return 5; + } + + std::array write_pdu{}; + const auto write_encoded = build_ten_variable_write(write_pdu); + const auto write_tpkt = write_encoded.success() + ? build_mms_tpkt( + std::span{write_pdu}.first(write_encoded.bytes_written), + request, + presentation, + scratch) + : wire::EncodeResult{}; + if (!write_tpkt.success()) { + return 13; + } + const auto write_result = runtime.process_tcp_window( + std::span{request}.first(write_tpkt.bytes_written), + response, + workspace); + if (write_result.status != mms::MmsStaticConnectionStatus::peer_limit_exceeded || + write_result.bytes_written != 0U || write_state) { + return 14; + } + + // The two large Read results exceed the peer's 64-byte MMS PDU offer. No + // oversized confirmed response may be placed on the wire. + const auto read_tpkt = build_mms_tpkt( + kReadRequest, request, presentation, scratch); + if (!read_tpkt.success()) { + return 6; + } + const auto read_result = runtime.process_tcp_window( + std::span{request}.first(read_tpkt.bytes_written), + response, + workspace); + if (read_result.status != mms::MmsStaticConnectionStatus::peer_limit_exceeded || + read_result.bytes_written != 0U || + read_result.consumed_bytes != read_tpkt.bytes_written || + runtime.state() != mms::MmsStaticConnectionState::established) { + return 7; + } + + // InformationReport has the same peer MMS/COTP bounds. Retain the entry and + // cursor when delivery is refused so it can be retried on a compatible peer. + const mms::MmsStaticBrcbDefinition definition{ + "LD0", "LLN0$BR$B1", "LD0/LLN0$BR$B1", + "LD0", "LLN0$Events", 1U, {0x7FU,0x80U}, 0U, 0x70U}; + std::array slot_storage{}; + std::array slots{ + mms::MmsStaticBrcbSlot{slot_storage}}; + mms::MmsStaticBrcbPendingState pending{}; + mms::MmsStaticBrcbRuntime reports{ + definition, pending, slots, object_table, data_set_table}; + if (!reports.initialize()) { + return 8; + } + mms::MmsStaticBrcbControl control{reports}; + mms::MmsStaticBrcbClientIdentity client; + client.association_id = 101U; + client.owner[0] = 0xAAU; + client.owner[1] = 0x01U; + client.owner_size = 2U; + if (control.reserve(client, 0U, 100U) != mms::MmsStaticBrcbControlStatus::ok || + control.set_report_enabled(client, true, 100U) != + mms::MmsStaticBrcbControlStatus::ok || + reports.notify(0U, mms::MmsStaticBrcbEventReason::data_change, 100U) != + mms::MmsStaticBrcbStatus::ok) { + return 9; + } + mms::MmsStaticBrcbCapturePlan plan; + std::array encode_buffer{}; + std::array capture_workspace{}; + constexpr std::array report_time{0U,0U,0x12U,0x34U,0U,0x01U}; + if (!reports.next_due(100U, plan) || + !reports.capture(plan, report_time, encode_buffer, capture_workspace).success() || + reports.queue_size() != 1U) { + return 10; + } + const auto report = mms::MmsStaticBrcbConnection::poll( + runtime, control, reports, 100U, response, workspace); + if (report.status != mms::MmsStaticBrcbConnectionStatus::peer_limit_exceeded || + report.bytes_written != 0U || reports.queue_size() != 1U) { + return 11; + } + + runtime.reset(); + if (runtime.negotiated_tpdu_size_bytes() != 0U || + runtime.negotiated_mms_pdu_size() != 0U) { + return 12; + } + return 0; +} diff --git a/embedded/mms_pdu_hard_profile_smoke.cpp b/embedded/mms_pdu_hard_profile_smoke.cpp new file mode 100644 index 000000000..89ebf9079 --- /dev/null +++ b/embedded/mms_pdu_hard_profile_smoke.cpp @@ -0,0 +1,255 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/pdu_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kInitiateRequest{ + 0xA8U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0xEEU, 0x1CU, 0x00U, 0x00U, 0x04U, + 0x08U, 0x00U, 0x00U, 0x79U, 0xEFU, 0x18U}; + +constexpr std::array kInitiateResponse{ + 0xA9U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +constexpr std::array kParameterSupport{ + 0x05U, 0xF1U, 0x00U}; +constexpr std::array kServicesSupportedRequest{ + 0x03U, 0xEEU, 0x1CU, 0x00U, 0x00U, 0x04U, + 0x08U, 0x00U, 0x00U, 0x79U, 0xEFU, 0x18U}; +constexpr std::array kServicesSupportedResponse{ + 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] bool valid_initiate( + const mms::MmsInitiateView& value, + const mms::MmsWirePduKind expected_kind, + const std::span expected_services) noexcept { + return value.kind == expected_kind && + value.maximum_mms_pdu_size == 65'000U && + value.maximum_outstanding_calling == 10U && + value.maximum_outstanding_called == 10U && + value.data_structure_nesting_level == 5U && + value.detail.version_number == 1U && + matches(value.detail.parameter_support_options, kParameterSupport) && + value.detail.services_supported_calling.size() == expected_services.size() && + std::equal( + value.detail.services_supported_calling.begin(), + value.detail.services_supported_calling.end(), + expected_services.begin()); +} + +} // namespace + +int main() { + mms::MmsInitiateView initiate; + if (!mms::MmsPduSpanCodec::try_decode_initiate_request_view( + kInitiateRequest, initiate) || + !valid_initiate( + initiate, + mms::MmsWirePduKind::initiate_request, + kServicesSupportedRequest)) { + return 1; + } + + if (!mms::MmsPduSpanCodec::try_decode_initiate_response_view( + kInitiateResponse, initiate) || + !valid_initiate( + initiate, + mms::MmsWirePduKind::initiate_response, + kServicesSupportedResponse)) { + return 2; + } + + std::array buffer{}; + const auto initiate_result = + mms::MmsPduSpanCodec::encode_default_initiate_response_into(buffer); + if (!initiate_result.success() || + initiate_result.bytes_written != kInitiateResponse.size() || + !matches( + std::span{buffer}.first( + initiate_result.bytes_written), + kInitiateResponse)) { + return 3; + } + + const auto dynamic_default = mms::MmsPduSpanCodec::encode_initiate_response_into( + 65'000U, 10U, 10U, 5U, buffer); + if (!dynamic_default.success() || + dynamic_default.bytes_written != kInitiateResponse.size() || + !matches( + std::span{buffer}.first(dynamic_default.bytes_written), + kInitiateResponse)) { + return 30; + } + + const auto negotiated = mms::MmsPduSpanCodec::encode_initiate_response_into( + 1'024U, 2U, 3U, 1U, buffer); + if (!negotiated.success() || + !mms::MmsPduSpanCodec::try_decode_initiate_response_view( + std::span{buffer}.first(negotiated.bytes_written), + initiate) || + initiate.maximum_mms_pdu_size != 1'024U || + initiate.maximum_outstanding_calling != 2U || + initiate.maximum_outstanding_called != 3U || + initiate.data_structure_nesting_level != 1U) { + return 31; + } + + constexpr std::array confirmed_request_golden{ + 0xA0U, 0x05U, 0x02U, 0x01U, 0x07U, 0xA4U, 0x00U}; + const auto confirmed_request_result = + mms::MmsPduSpanCodec::encode_confirmed_request_into( + 7U, 4, true, {}, buffer); + if (!confirmed_request_result.success() || + confirmed_request_result.bytes_written != confirmed_request_golden.size() || + !matches( + std::span{buffer}.first( + confirmed_request_result.bytes_written), + confirmed_request_golden)) { + return 4; + } + + mms::MmsConfirmedPduView confirmed; + if (!mms::MmsPduSpanCodec::try_decode_confirmed_request_view( + confirmed_request_golden, confirmed) || + confirmed.kind != mms::MmsWirePduKind::confirmed_request || + confirmed.invoke_id != 7U || confirmed.service_tag != 4 || + confirmed.service() != mms::MmsWireConfirmedService::read || + !confirmed.service_constructed || !confirmed.service_value.empty()) { + return 5; + } + + constexpr std::array confirmed_response_golden{ + 0xA1U, 0x05U, 0x02U, 0x01U, 0x07U, 0xA4U, 0x00U}; + const auto confirmed_response_result = + mms::MmsPduSpanCodec::encode_confirmed_response_into( + 7U, 4, true, {}, buffer); + if (!confirmed_response_result.success() || + confirmed_response_result.bytes_written != confirmed_response_golden.size() || + !matches( + std::span{buffer}.first( + confirmed_response_result.bytes_written), + confirmed_response_golden) || + !mms::MmsPduSpanCodec::try_decode_confirmed_response_view( + confirmed_response_golden, confirmed) || + confirmed.kind != mms::MmsWirePduKind::confirmed_response || + confirmed.invoke_id != 7U || confirmed.service_tag != 4) { + return 6; + } + + constexpr std::array reject_golden{ + 0xA4U, 0x06U, 0x80U, 0x01U, 0x07U, 0x81U, 0x01U, 0x01U}; + const auto reject_result = + mms::MmsPduSpanCodec::encode_confirmed_request_reject_into( + 7U, + mms::MmsConfirmedRequestRejectReason::unrecognized_service, + buffer); + if (!reject_result.success() || + reject_result.bytes_written != reject_golden.size() || + !matches( + std::span{buffer}.first(reject_result.bytes_written), + reject_golden)) { + return 7; + } + + constexpr std::array maximum_invoke_golden{ + 0xA0U, 0x08U, 0x02U, 0x04U, + 0x7FU, 0xFFU, 0xFFU, 0xFFU, + 0xA4U, 0x00U}; + const auto maximum_result = mms::MmsPduSpanCodec::encode_confirmed_request_into( + mms::MmsPduSpanCodec::maximum_invoke_id, 4, true, {}, buffer); + if (!maximum_result.success() || + maximum_result.bytes_written != maximum_invoke_golden.size() || + !matches( + std::span{buffer}.first(maximum_result.bytes_written), + maximum_invoke_golden) || + !mms::MmsPduSpanCodec::try_decode_confirmed_request_view( + maximum_invoke_golden, confirmed) || + confirmed.invoke_id != mms::MmsPduSpanCodec::maximum_invoke_id) { + return 8; + } + + constexpr std::array invoke_128_golden{ + 0xA0U, 0x06U, 0x02U, 0x02U, 0x00U, 0x80U, 0xA4U, 0x00U}; + const auto invoke_128 = mms::MmsPduSpanCodec::encode_confirmed_request_into( + 128U, 4, true, {}, buffer); + if (!invoke_128.success() || invoke_128.bytes_written != invoke_128_golden.size() || + !matches( + std::span{buffer}.first(invoke_128.bytes_written), + invoke_128_golden)) { + return 9; + } + + std::array p_data{}; + const auto p_data_result = osi::PresentationSpanCodec::encode_p_data_into( + confirmed_request_golden, p_data, 3U, true); + osi::PresentationPdvView pdv; + if (!p_data_result.success() || + !osi::PresentationSpanCodec::try_decode_p_data_view( + std::span{p_data}.first(p_data_result.bytes_written), + pdv) || + pdv.context_id != 3U || + !mms::MmsPduSpanCodec::try_decode_confirmed_request_view( + pdv.single_asn1_type, confirmed) || + confirmed.invoke_id != 7U || + confirmed.service() != mms::MmsWireConfirmedService::read) { + return 10; + } + + for (std::uint32_t iteration = 0U; iteration < 50'000U; ++iteration) { + const auto invoke = iteration & 0x7FFFU; + const auto encoded = mms::MmsPduSpanCodec::encode_confirmed_response_into( + invoke, 4, true, {}, buffer); + mms::MmsConfirmedPduView decoded; + if (!encoded.success() || + !mms::MmsPduSpanCodec::try_decode_confirmed_response_view( + std::span{buffer}.first(encoded.bytes_written), + decoded) || + decoded.invoke_id != invoke || decoded.service_tag != 4) { + return 11; + } + } + + constexpr std::array malformed_initiate{ + 0xA8U, 0x03U, 0x80U, 0x01U, 0x01U}; + if (mms::MmsPduSpanCodec::try_decode_initiate_request_view( + malformed_initiate, initiate)) { + return 12; + } + + return 0; +} diff --git a/embedded/mms_static_model_hard_profile_smoke.cpp b/embedded/mms_static_model_hard_profile_smoke.cpp new file mode 100644 index 000000000..f5ccac663 --- /dev/null +++ b/embedded/mms_static_model_hard_profile_smoke.cpp @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kInt32Type{0x85U, 0x01U, 0x20U}; + +constexpr std::array kAttributesRequest{ + 0xA0U, 0x12U, 0x02U, 0x01U, 0x0BU, + 0xA6U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U}; + +constexpr std::array kAttributesResponse{ + 0xA1U, 0x0CU, 0x02U, 0x01U, 0x0BU, + 0xA6U, 0x07U, 0x80U, 0x01U, 0x00U, + 0xA2U, 0x02U, 0x83U, 0x00U}; + +constexpr std::array kReadRequest{ + 0xA0U, 0x27U, 0x02U, 0x01U, 0x0CU, + 0xA4U, 0x22U, 0xA1U, 0x20U, 0xA0U, 0x1EU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x4DU, 0x31U}; + +constexpr std::array kReadResponse{ + 0xA1U, 0x0DU, 0x02U, 0x01U, 0x0CU, + 0xA4U, 0x08U, 0xA1U, 0x06U, + 0x83U, 0x01U, 0xFFU, + 0x85U, 0x01U, 0x2AU}; + +constexpr std::array kLd0{0x4CU, 0x44U, 0x30U}; +constexpr std::array kR1{0x52U, 0x31U}; +constexpr std::array kM1{0x4DU, 0x31U}; +constexpr std::array kBooleanData{0x83U, 0x01U, 0xFFU}; +constexpr std::array kIntData{0x85U, 0x01U, 0x2AU}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto value = *static_cast(context); + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] wire::EncodeResult read_small_int32( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + const auto value = *static_cast(context); + if (value > 0x7FU) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x85U; + destination[1] = 0x01U; + destination[2] = value; + return {wire::EncodeStatus::ok, required, required}; +} + +} // namespace + +int main() { + const bool relay_state = true; + const std::uint8_t meter_value = 42U; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "R1", kBooleanType, read_boolean, &relay_state, false}, + mms::MmsStaticObjectEntry{ + "LD0", "M1", kInt32Type, read_small_int32, &meter_value, false}}; + const mms::MmsStaticObjectTable table{objects}; + if (!table.valid()) { + return 1; + } + + mms::MmsVariableAccessAttributesRequestView attributes_request; + if (!mms::MmsServiceSpanCodec::try_decode_variable_access_attributes_request( + kAttributesRequest, attributes_request) || + attributes_request.invoke_id != 11U || + attributes_request.name.kind != mms::MmsObjectNameViewKind::domain_specific || + !matches(attributes_request.name.domain, kLd0) || + !matches(attributes_request.name.item, kR1)) { + return 2; + } + const auto* relay = table.find(attributes_request.name); + if (relay == nullptr || relay->type_specification.data() != kBooleanType.data()) { + return 3; + } + + std::array buffer{}; + const auto attributes_encoded = + mms::MmsServiceSpanCodec::encode_variable_access_attributes_response_into( + attributes_request.invoke_id, + relay->mms_deletable, + relay->type_specification, + buffer); + if (!attributes_encoded.success() || + attributes_encoded.bytes_written != kAttributesResponse.size() || + !matches( + std::span{buffer}.first(attributes_encoded.bytes_written), + kAttributesResponse)) { + return 4; + } + + mms::MmsVariableAccessAttributesResponseView attributes_response; + if (!mms::MmsServiceSpanCodec::try_decode_variable_access_attributes_response( + kAttributesResponse, attributes_response) || + attributes_response.invoke_id != 11U || attributes_response.mms_deletable || + !matches(attributes_response.type_specification, kBooleanType)) { + return 5; + } + + mms::MmsReadRequestView read_request; + if (!mms::MmsServiceSpanCodec::try_decode_read_request(kReadRequest, read_request) || + read_request.invoke_id != 12U || read_request.specification_with_result || + read_request.variable_count != 2U) { + return 6; + } + + std::array resolved{}; + std::size_t resolved_count = 0U; + if (!table.try_resolve_read_request(read_request, resolved, resolved_count) || + resolved_count != 2U || resolved[0] != &objects[0] || resolved[1] != &objects[1]) { + return 7; + } + + mms::MmsObjectNameView variable; + if (!read_request.try_variable(0U, variable) || !matches(variable.item, kR1) || + !read_request.try_variable(1U, variable) || !matches(variable.item, kM1) || + read_request.try_variable(2U, variable)) { + return 8; + } + + std::array relay_data{}; + std::array meter_data{}; + const auto relay_read = resolved[0]->read(resolved[0]->context, relay_data); + const auto meter_read = resolved[1]->read(resolved[1]->context, meter_data); + if (!relay_read.success() || !meter_read.success() || + !matches( + std::span{relay_data}.first(relay_read.bytes_written), + kBooleanData) || + !matches( + std::span{meter_data}.first(meter_read.bytes_written), + kIntData)) { + return 9; + } + + const std::array results{ + mms::MmsReadAccessResultInput{ + true, + std::span{relay_data}.first(relay_read.bytes_written), + 0U}, + mms::MmsReadAccessResultInput{ + true, + std::span{meter_data}.first(meter_read.bytes_written), + 0U}}; + const auto read_encoded = mms::MmsServiceSpanCodec::encode_read_response_into( + read_request.invoke_id, results, buffer); + if (!read_encoded.success() || read_encoded.bytes_written != kReadResponse.size() || + !matches( + std::span{buffer}.first(read_encoded.bytes_written), + kReadResponse)) { + return 10; + } + + mms::MmsReadResponseView read_response; + if (!mms::MmsServiceSpanCodec::try_decode_read_response( + kReadResponse, read_response) || + read_response.invoke_id != 12U || read_response.result_count != 2U) { + return 11; + } + mms::MmsReadAccessResultView access; + if (!read_response.try_result(0U, access) || !access.success || + !matches(access.encoded_data, kBooleanData) || + !read_response.try_result(1U, access) || !access.success || + !matches(access.encoded_data, kIntData) || + read_response.try_result(2U, access)) { + return 12; + } + + // Failure results remain bounded and explicit; no exception or owning object + // graph is required to return an MMS DataAccessError. + const std::array failed{ + mms::MmsReadAccessResultInput{false, {}, 10U}}; + const auto failed_encoded = mms::MmsServiceSpanCodec::encode_read_response_into( + 13U, failed, buffer); + if (!failed_encoded.success() || + !mms::MmsServiceSpanCodec::try_decode_read_response( + std::span{buffer}.first(failed_encoded.bytes_written), + read_response) || + read_response.result_count != 1U || + !read_response.try_result(0U, access) || access.success || access.failure_code != 10U) { + return 13; + } + + for (std::uint32_t iteration = 0U; iteration < 50'000U; ++iteration) { + const auto invoke = iteration & 0x7FFFU; + const auto encoded = mms::MmsServiceSpanCodec::encode_read_response_into( + invoke, results, buffer); + mms::MmsReadResponseView decoded; + if (!encoded.success() || + !mms::MmsServiceSpanCodec::try_decode_read_response( + std::span{buffer}.first(encoded.bytes_written), + decoded) || + decoded.invoke_id != invoke || decoded.result_count != 2U) { + return 14; + } + } + + const std::array duplicates{ + objects[0], objects[0]}; + if (mms::MmsStaticObjectTable{duplicates}.valid()) { + return 15; + } + + return 0; +} diff --git a/embedded/mms_write_hard_profile_smoke.cpp b/embedded/mms_write_hard_profile_smoke.cpp new file mode 100644 index 000000000..7be9e886a --- /dev/null +++ b/embedded/mms_write_hard_profile_smoke.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kInt32Type{0x85U, 0x01U, 0x20U}; + +constexpr std::array kWriteRequest{ + 0xA0U, 0x1BU, 0x02U, 0x01U, 0x0EU, + 0xA5U, 0x16U, + 0xA0U, 0x0FU, + 0x30U, 0x0DU, 0xA0U, 0x0BU, 0xA1U, 0x09U, + 0x1AU, 0x03U, 0x4CU, 0x44U, 0x30U, + 0x1AU, 0x02U, 0x52U, 0x31U, + 0xA0U, 0x03U, 0x83U, 0x01U, 0x00U}; + +constexpr std::array kWriteSuccessResponse{ + 0xA1U, 0x07U, 0x02U, 0x01U, 0x0EU, + 0xA5U, 0x02U, 0x81U, 0x00U}; + +constexpr std::array kWriteFailureResponse{ + 0xA1U, 0x08U, 0x02U, 0x01U, 0x0FU, + 0xA5U, 0x03U, 0x80U, 0x01U, 0x03U}; + +constexpr std::array kLd0{0x4CU, 0x44U, 0x30U}; +constexpr std::array kR1{0x52U, 0x31U}; +constexpr std::array kM1{0x4DU, 0x31U}; +constexpr std::array kFalseData{0x83U, 0x01U, 0x00U}; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto value = *static_cast(context); + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] wire::EncodeResult read_small_int32( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + const auto value = *static_cast(context); + if (value > 0x7FU) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x85U; + destination[1] = 0x01U; + destination[2] = value; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] mms::MmsStaticWriteResult write_boolean( + void* context, + const std::span encoded_data) noexcept { + if (context == nullptr || encoded_data.size() != 3U || + encoded_data[0] != 0x83U || encoded_data[1] != 0x01U || + (encoded_data[2] != 0x00U && encoded_data[2] != 0xFFU)) { + return {false, 7U}; + } + *static_cast(context) = encoded_data[2] != 0x00U; + return {true, 0U}; +} + +} // namespace + +int main() { + bool relay_state = true; + const std::uint8_t meter_value = 42U; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "R1", kBooleanType, + read_boolean, &relay_state, false, + write_boolean, &relay_state}, + mms::MmsStaticObjectEntry{ + "LD0", "M1", kInt32Type, + read_small_int32, &meter_value, false}}; + const mms::MmsStaticObjectTable table{objects}; + if (!table.valid() || !objects[0].writable() || objects[1].writable()) { + return 1; + } + + mms::MmsWriteRequestView request; + if (!mms::MmsServiceSpanCodec::try_decode_write_request(kWriteRequest, request) || + request.invoke_id != 14U || request.variable_count != 1U || request.data_count != 1U) { + return 2; + } + mms::MmsObjectNameView name; + std::span value; + if (!request.try_variable(0U, name) || + name.kind != mms::MmsObjectNameViewKind::domain_specific || + !matches(name.domain, kLd0) || !matches(name.item, kR1) || + !request.try_value(0U, value) || !matches(value, kFalseData) || + request.try_variable(1U, name) || request.try_value(1U, value)) { + return 3; + } + + std::array resolved{}; + std::size_t resolved_count = 0U; + if (!table.try_resolve_write_request(request, resolved, resolved_count) || + resolved_count != 1U || resolved[0] != &objects[0] || !resolved[0]->writable()) { + return 4; + } + if (!request.try_value(0U, value)) { + return 5; + } + const auto write_result = resolved[0]->write(resolved[0]->write_context, value); + if (!write_result.success || relay_state) { + return 6; + } + + std::array buffer{}; + const std::array success{ + mms::MmsWriteAccessResultInput{true, 0U}}; + const auto success_encoded = mms::MmsServiceSpanCodec::encode_write_response_into( + request.invoke_id, success, buffer); + if (!success_encoded.success() || + success_encoded.bytes_written != kWriteSuccessResponse.size() || + !matches( + std::span{buffer}.first(success_encoded.bytes_written), + kWriteSuccessResponse)) { + return 7; + } + + mms::MmsWriteResponseView response; + mms::MmsWriteAccessResultView access; + if (!mms::MmsServiceSpanCodec::try_decode_write_response( + kWriteSuccessResponse, response) || + response.invoke_id != 14U || response.result_count != 1U || + !response.try_result(0U, access) || !access.success || + response.try_result(1U, access)) { + return 8; + } + + auto read_only_request = kWriteRequest; + read_only_request[4] = 0x0FU; + read_only_request[22] = 0x4DU; + if (!mms::MmsServiceSpanCodec::try_decode_write_request( + read_only_request, request) || + !request.try_variable(0U, name) || !matches(name.item, kM1) || + !table.try_resolve_write_request(request, resolved, resolved_count) || + resolved_count != 1U || resolved[0] != &objects[1] || resolved[0]->writable()) { + return 9; + } + + const std::array denied{ + mms::MmsWriteAccessResultInput{false, 3U}}; + const auto denied_encoded = mms::MmsServiceSpanCodec::encode_write_response_into( + request.invoke_id, denied, buffer); + if (!denied_encoded.success() || + denied_encoded.bytes_written != kWriteFailureResponse.size() || + !matches( + std::span{buffer}.first(denied_encoded.bytes_written), + kWriteFailureResponse) || + !mms::MmsServiceSpanCodec::try_decode_write_response( + kWriteFailureResponse, response) || + !response.try_result(0U, access) || access.success || access.failure_code != 3U) { + return 10; + } + + auto bad_type_request = kWriteRequest; + bad_type_request[26] = 0x85U; + if (!mms::MmsServiceSpanCodec::try_decode_write_request( + bad_type_request, request) || + !table.try_resolve_write_request(request, resolved, resolved_count) || + resolved[0] == nullptr || !resolved[0]->writable() || + !request.try_value(0U, value)) { + return 11; + } + const auto rejected = resolved[0]->write(resolved[0]->write_context, value); + if (rejected.success || rejected.failure_code != 7U || relay_state) { + return 12; + } + + for (std::uint32_t iteration = 0U; iteration < 50'000U; ++iteration) { + const auto invoke = iteration & 0x7FFFU; + const auto encoded = mms::MmsServiceSpanCodec::encode_write_response_into( + invoke, (iteration & 1U) == 0U ? success : denied, buffer); + mms::MmsWriteResponseView decoded; + if (!encoded.success() || + !mms::MmsServiceSpanCodec::try_decode_write_response( + std::span{buffer}.first(encoded.bytes_written), + decoded) || + decoded.invoke_id != invoke || decoded.result_count != 1U || + !decoded.try_result(0U, access) || + access.success != ((iteration & 1U) == 0U)) { + return 13; + } + } + + return 0; +} diff --git a/embedded/osi_hard_profile_smoke.cpp b/embedded/osi_hard_profile_smoke.cpp new file mode 100644 index 000000000..cb4ff0d17 --- /dev/null +++ b/embedded/osi_hard_profile_smoke.cpp @@ -0,0 +1,234 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850::osi; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] bool parameter_matches( + const CotpTpduView& tpdu, + const std::uint8_t code, + const std::span expected) noexcept { + std::span value; + return tpdu.try_parameter(code, value) && + value.size() == expected.size() && + std::equal(value.begin(), value.end(), expected.begin()); +} + +} // namespace + +int main() { + // Existing desktop golden vector: TPKT wrapping a COTP Data TPDU prefix. + constexpr std::array tpkt_payload{ + 0x02U, 0xF0U, 0x80U, 0xA8U, 0x01U, 0x00U}; + constexpr std::array tpkt_golden{ + 0x03U, 0x00U, 0x00U, 0x0AU, + 0x02U, 0xF0U, 0x80U, 0xA8U, 0x01U, 0x00U}; + + std::array tpkt_buffer{}; + const auto tpkt_result = TpktSpanCodec::encode_into(tpkt_payload, tpkt_buffer); + if (!tpkt_result.success() || tpkt_result.bytes_written != tpkt_golden.size() || + !matches( + std::span{tpkt_buffer}.first(tpkt_result.bytes_written), + tpkt_golden)) { + return 1; + } + + TpktFrameView tpkt_view; + if (!TpktSpanCodec::try_decode_view(tpkt_golden, tpkt_view) || + tpkt_view.version != TpktSpanCodec::supported_version || + tpkt_view.declared_length != tpkt_golden.size() || + !matches(tpkt_view.payload, tpkt_payload)) { + return 2; + } + + if (TpktSpanCodec::peek_frame( + std::span{tpkt_golden}.first(2U)).status != + TpktPeekStatus::need_more) { + return 3; + } + + std::array coalesced{}; + std::copy(tpkt_golden.begin(), tpkt_golden.end(), coalesced.begin()); + coalesced.back() = 0xFFU; + const auto peek = TpktSpanCodec::peek_frame(coalesced); + if (!peek.ready() || peek.frame_bytes != tpkt_golden.size()) { + return 4; + } + + constexpr std::array invalid_tpkt{ + 0x02U, 0x00U, 0x00U, 0x04U}; + if (TpktSpanCodec::peek_frame(invalid_tpkt).status != TpktPeekStatus::invalid || + TpktSpanCodec::try_decode_view(invalid_tpkt, tpkt_view)) { + return 5; + } + + // Existing desktop golden vector: default COTP Connection Request. + constexpr std::array tpdu_size{0x0AU}; + constexpr std::array source_tsap{0x00U, 0x01U}; + constexpr std::array destination_tsap{0x00U, 0x01U}; + const std::array default_parameters{ + CotpParameterView{CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + CotpParameterView{CotpSpanCodec::source_tsap_parameter, source_tsap}, + CotpParameterView{CotpSpanCodec::destination_tsap_parameter, destination_tsap}, + }; + constexpr std::array cr_golden{ + 0x11U, 0xE0U, 0x00U, 0x00U, 0x00U, 0x01U, 0x00U, + 0xC0U, 0x01U, 0x0AU, + 0xC1U, 0x02U, 0x00U, 0x01U, + 0xC2U, 0x02U, 0x00U, 0x01U}; + + std::array cr_buffer{}; + const auto cr_result = CotpSpanCodec::encode_connection_request_into( + 0x0001U, default_parameters, cr_buffer); + if (!cr_result.success() || cr_result.bytes_written != cr_golden.size() || + !matches( + std::span{cr_buffer}.first(cr_result.bytes_written), + cr_golden)) { + return 6; + } + + CotpTpduView cr_view; + if (!CotpSpanCodec::try_decode_view(cr_golden, cr_view) || + cr_view.kind != CotpWireKind::connection_request || + cr_view.destination_reference != 0x0000U || + cr_view.source_reference != 0x0001U || + !parameter_matches(cr_view, CotpSpanCodec::tpdu_size_parameter, tpdu_size) || + !parameter_matches(cr_view, CotpSpanCodec::source_tsap_parameter, source_tsap) || + !parameter_matches( + cr_view, CotpSpanCodec::destination_tsap_parameter, destination_tsap)) { + return 7; + } + + // Server-side CC must negotiate TPDU size and mirror TSAP values without + // constructing an owning parameter list. + constexpr std::array offered_size{0x09U}; + constexpr std::array offered_source{0x11U, 0x22U}; + constexpr std::array offered_destination{0x33U, 0x44U, 0x55U}; + const std::array offered_parameters{ + CotpParameterView{CotpSpanCodec::tpdu_size_parameter, offered_size}, + CotpParameterView{CotpSpanCodec::source_tsap_parameter, offered_source}, + CotpParameterView{CotpSpanCodec::destination_tsap_parameter, offered_destination}, + }; + + std::array request_buffer{}; + const auto request_result = CotpSpanCodec::encode_connection_request_into( + 0x0001U, offered_parameters, request_buffer); + CotpTpduView request_view; + if (!request_result.success() || + !CotpSpanCodec::try_decode_view( + std::span{request_buffer}.first(request_result.bytes_written), + request_view)) { + return 8; + } + + constexpr std::array cc_golden{ + 0x12U, 0xD0U, 0x00U, 0x01U, 0x10U, 0x01U, 0x00U, + 0xC0U, 0x01U, 0x09U, + 0xC2U, 0x02U, 0x11U, 0x22U, + 0xC1U, 0x03U, 0x33U, 0x44U, 0x55U}; + std::array cc_buffer{}; + const auto cc_result = CotpSpanCodec::encode_connection_confirm_from_request_into( + request_view, 0x1001U, cc_buffer, 0x0AU); + if (!cc_result.success() || cc_result.bytes_written != cc_golden.size() || + !matches( + std::span{cc_buffer}.first(cc_result.bytes_written), + cc_golden)) { + return 9; + } + + CotpTpduView cc_view; + if (!CotpSpanCodec::try_decode_view(cc_golden, cc_view) || + cc_view.kind != CotpWireKind::connection_confirm || + cc_view.destination_reference != 0x0001U || + cc_view.source_reference != 0x1001U || + !parameter_matches(cc_view, CotpSpanCodec::tpdu_size_parameter, offered_size) || + !parameter_matches(cc_view, CotpSpanCodec::source_tsap_parameter, offered_destination) || + !parameter_matches(cc_view, CotpSpanCodec::destination_tsap_parameter, offered_source)) { + return 10; + } + + constexpr std::array cotp_user_data{ + 0x0DU, 0x01U, 0x02U, 0x03U}; + constexpr std::array cotp_data_golden{ + 0x02U, 0xF0U, 0x80U, 0x0DU, 0x01U, 0x02U, 0x03U}; + std::array cotp_data_buffer{}; + const auto data_result = CotpSpanCodec::encode_data_into( + cotp_user_data, cotp_data_buffer); + if (!data_result.success() || data_result.bytes_written != cotp_data_golden.size() || + !matches( + std::span{cotp_data_buffer}.first(data_result.bytes_written), + cotp_data_golden)) { + return 11; + } + + CotpTpduView data_view; + if (!CotpSpanCodec::try_decode_view(cotp_data_golden, data_view) || + data_view.kind != CotpWireKind::data || + !data_view.end_of_transmission || data_view.tpdu_number != 0U || + !matches(data_view.user_data, cotp_user_data)) { + return 12; + } + + // Complete zero-copy transport stack: COTP Data -> TPKT -> view decode -> + // COTP view decode, retaining the same caller-owned storage throughout. + std::array transport_buffer{}; + const auto transport_result = TpktSpanCodec::encode_into( + std::span{cotp_data_buffer}.first(data_result.bytes_written), + transport_buffer); + TpktFrameView transport_view; + CotpTpduView nested_data_view; + if (!transport_result.success() || + !TpktSpanCodec::try_decode_view( + std::span{transport_buffer}.first( + transport_result.bytes_written), + transport_view) || + !CotpSpanCodec::try_decode_view(transport_view.payload, nested_data_view) || + !matches(nested_data_view.user_data, cotp_user_data)) { + return 13; + } + + // Exercise the steady-state bounded path repeatedly. No owning container is + // constructed by either codec during these encode/decode iterations. + for (std::uint32_t iteration = 0U; iteration < 50'000U; ++iteration) { + const auto encoded = CotpSpanCodec::encode_data_into( + cotp_user_data, cotp_data_buffer, true, + static_cast(iteration & 0x7FU)); + if (!encoded.success()) { + return 14; + } + CotpTpduView decoded; + if (!CotpSpanCodec::try_decode_view( + std::span{cotp_data_buffer}.first( + encoded.bytes_written), + decoded) || + decoded.tpdu_number != static_cast(iteration & 0x7FU)) { + return 15; + } + } + + constexpr std::array malformed_parameter{ + 0x09U, 0xE0U, 0x00U, 0x00U, 0x00U, 0x01U, 0x00U, + 0xC0U, 0x02U, 0x0AU}; + if (CotpSpanCodec::try_decode_view(malformed_parameter, cr_view)) { + return 16; + } + + return 0; +} diff --git a/embedded/session_presentation_hard_profile_smoke.cpp b/embedded/session_presentation_hard_profile_smoke.cpp new file mode 100644 index 000000000..c43cde56e --- /dev/null +++ b/embedded/session_presentation_hard_profile_smoke.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +template +[[nodiscard]] bool matches( + const std::span actual, + const std::array& expected) noexcept { + return actual.size() == expected.size() && + std::equal(actual.begin(), actual.end(), expected.begin()); +} + +[[nodiscard]] bool parameter_matches( + const osi::SessionSpduView& spdu, + const std::uint8_t code, + const std::span expected) noexcept { + std::span value; + return spdu.try_parameter(code, value) && + value.size() == expected.size() && + std::equal(value.begin(), value.end(), expected.begin()); +} + +} // namespace + +int main() { + constexpr std::array mms_payload{ + 0xA0U, 0x08U, 0x02U, 0x01U, 0x07U, + 0xA4U, 0x03U, 0x80U, 0x01U, 0x00U}; + constexpr std::array presentation_golden{ + 0x61U, 0x11U, 0x30U, 0x0FU, + 0x02U, 0x01U, 0x03U, + 0xA0U, 0x0AU, + 0xA0U, 0x08U, 0x02U, 0x01U, 0x07U, + 0xA4U, 0x03U, 0x80U, 0x01U, 0x00U}; + + // BER reader stays zero-copy and rejects indefinite-length BER. + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(presentation_golden, outer) || + outer.tag_class != asn1::BerClass::application || + outer.tag_number != 1 || !outer.constructed) { + return 1; + } + constexpr std::array indefinite{ + 0x61U, 0x80U, 0x00U, 0x00U}; + if (asn1::BerSpanReader::try_read_exact(indefinite, outer)) { + return 2; + } + + // Presentation fully-encoded-data must match the existing host encoding. + std::array presentation_buffer{}; + const auto presentation_result = + osi::PresentationSpanCodec::encode_fully_encoded_data_into( + 3U, mms_payload, presentation_buffer); + if (!presentation_result.success() || + presentation_result.bytes_written != presentation_golden.size() || + !matches( + std::span{presentation_buffer}.first( + presentation_result.bytes_written), + presentation_golden)) { + return 3; + } + + osi::PresentationPdvView pdv; + if (!osi::PresentationSpanCodec::try_decode_fully_encoded_data_view( + presentation_golden, pdv) || + pdv.context_id != 3U || !matches(pdv.single_asn1_type, mms_payload)) { + return 4; + } + + // Default Session parameters are byte-identical to the established host + // profile. The bounded encoder appends Presentation bytes as final C1 data. + constexpr std::array parameter05{ + 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U}; + constexpr std::array parameter14{0x00U, 0x02U}; + constexpr std::array parameter33{0x00U, 0x01U}; + constexpr std::array parameter34{0x00U, 0x01U}; + constexpr std::array session_connect_golden{ + 0x0DU, 0x29U, + 0x05U, 0x06U, 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U, + 0x14U, 0x02U, 0x00U, 0x02U, + 0x33U, 0x02U, 0x00U, 0x01U, + 0x34U, 0x02U, 0x00U, 0x01U, + 0xC1U, 0x13U, + 0x61U, 0x11U, 0x30U, 0x0FU, + 0x02U, 0x01U, 0x03U, + 0xA0U, 0x0AU, + 0xA0U, 0x08U, 0x02U, 0x01U, 0x07U, + 0xA4U, 0x03U, 0x80U, 0x01U, 0x00U}; + + std::array session_buffer{}; + const auto connect_result = osi::SessionSpanCodec::encode_connect_into( + presentation_golden, session_buffer); + if (!connect_result.success() || + connect_result.bytes_written != session_connect_golden.size() || + !matches( + std::span{session_buffer}.first( + connect_result.bytes_written), + session_connect_golden)) { + return 5; + } + + osi::SessionSpduView connect_view; + if (!osi::SessionSpanCodec::try_decode_view( + session_connect_golden, connect_view) || + connect_view.kind != osi::SessionWireKind::connect || + !matches(connect_view.user_data, presentation_golden) || + !parameter_matches(connect_view, 0x05U, parameter05) || + !parameter_matches(connect_view, 0x14U, parameter14) || + !parameter_matches(connect_view, 0x33U, parameter33) || + !parameter_matches(connect_view, 0x34U, parameter34)) { + return 6; + } + + constexpr std::array session_accept_golden{ + 0x0EU, 0x29U, + 0x05U, 0x06U, 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U, + 0x14U, 0x02U, 0x00U, 0x02U, + 0x33U, 0x02U, 0x00U, 0x01U, + 0x34U, 0x02U, 0x00U, 0x01U, + 0xC1U, 0x13U, + 0x61U, 0x11U, 0x30U, 0x0FU, + 0x02U, 0x01U, 0x03U, + 0xA0U, 0x0AU, + 0xA0U, 0x08U, 0x02U, 0x01U, 0x07U, + 0xA4U, 0x03U, 0x80U, 0x01U, 0x00U}; + const auto accept_result = osi::SessionSpanCodec::encode_accept_mirroring_into( + connect_view, presentation_golden, session_buffer); + if (!accept_result.success() || + accept_result.bytes_written != session_accept_golden.size() || + !matches( + std::span{session_buffer}.first( + accept_result.bytes_written), + session_accept_golden)) { + return 7; + } + + // Existing Session data-transfer profile: optional Give-Tokens prefix plus + // the Data-Transfer SPDU, followed directly by Presentation bytes. + constexpr std::array p_data_golden{ + 0x01U, 0x00U, 0x01U, 0x00U, + 0x61U, 0x11U, 0x30U, 0x0FU, + 0x02U, 0x01U, 0x03U, + 0xA0U, 0x0AU, + 0xA0U, 0x08U, 0x02U, 0x01U, 0x07U, + 0xA4U, 0x03U, 0x80U, 0x01U, 0x00U}; + + const auto p_data_result = osi::PresentationSpanCodec::encode_p_data_into( + mms_payload, presentation_buffer, 3U, true); + if (!p_data_result.success() || + p_data_result.bytes_written != p_data_golden.size() || + !matches( + std::span{presentation_buffer}.first( + p_data_result.bytes_written), + p_data_golden)) { + return 8; + } + + if (!osi::PresentationSpanCodec::try_decode_p_data_view(p_data_golden, pdv) || + pdv.context_id != 3U || !matches(pdv.single_asn1_type, mms_payload)) { + return 9; + } + + osi::SessionDataTransferView transfer; + if (!osi::SessionSpanCodec::try_decode_data_transfer_view( + p_data_golden, transfer) || + !transfer.has_give_tokens_prefix || + !matches(transfer.presentation_payload, presentation_golden)) { + return 10; + } + + // Direct Presentation decode remains accepted when the Session wrapper has + // already been removed by the transport state machine. + if (!osi::PresentationSpanCodec::try_decode_p_data_view( + presentation_golden, pdv) || + pdv.context_id != 3U || !matches(pdv.single_asn1_type, mms_payload)) { + return 11; + } + + // Steady-state encode/decode regression: no owning container is created by + // BER, Session, or Presentation bounded APIs. + for (std::uint32_t iteration = 0U; iteration < 50'000U; ++iteration) { + const auto context_id = (iteration & 1U) == 0U ? 3U : 5U; + const auto encoded = osi::PresentationSpanCodec::encode_p_data_into( + mms_payload, presentation_buffer, context_id, true); + if (!encoded.success()) { + return 12; + } + osi::PresentationPdvView decoded; + if (!osi::PresentationSpanCodec::try_decode_p_data_view( + std::span{presentation_buffer}.first( + encoded.bytes_written), + decoded) || + decoded.context_id != context_id || + !matches(decoded.single_asn1_type, mms_payload)) { + return 13; + } + } + + return 0; +} diff --git a/include/ariec61850/acse/association_span.hpp b/include/ariec61850/acse/association_span.hpp new file mode 100644 index 000000000..7e7337297 --- /dev/null +++ b/include/ariec61850/acse/association_span.hpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::acse { + +struct AcseExternalView final { + std::span direct_reference{}; + std::uint32_t indirect_reference{}; + std::span single_asn1_type{}; +}; + +struct AcseAarqView final { + std::span application_context_name{}; + std::span called_ap_title{}; + std::optional called_ae_qualifier{}; + std::span calling_ap_title{}; + std::optional calling_ae_qualifier{}; + AcseExternalView user_information{}; +}; + +struct AssociationRequestView final { + osi::SessionSpduView session{}; + osi::PresentationCpView presentation{}; + AcseAarqView aarq{}; + std::uint32_t acse_presentation_context_id{}; + std::uint32_t mms_presentation_context_id{}; +}; + +class AcseSpanCodec final { +public: + static constexpr std::size_t maximum_acse_bytes = 1U * 1024U * 1024U; + static constexpr std::size_t maximum_oid_bytes = 64U; + + [[nodiscard]] static std::span + mms_application_context_name() noexcept; + [[nodiscard]] static std::span + default_mms_initiate_request() noexcept; + [[nodiscard]] static std::span + default_mms_initiate_response() noexcept; + + [[nodiscard]] static bool try_decode_aarq_view( + std::span bytes, + AcseAarqView& aarq) noexcept; + + // Strict protocol decoder used by conformance-oriented/offline paths. + [[nodiscard]] static bool try_decode_association_request_view( + std::span bytes, + AssociationRequestView& request) noexcept; + + // Bounded engineering-client compatibility decoder. It prefers the strict + // path, then falls back to the tolerant association-inspection behavior + // used by the proven ARIEC61850 simulator/server: Session Connect, + // Presentation contexts, AARQ UserInformation and MMS InitiateRequest are + // required, while harmless optional Presentation/ACSE decorations are + // ignored rather than turning into a disconnect. + [[nodiscard]] static bool try_decode_association_request_compat_view( + std::span bytes, + AssociationRequestView& request) noexcept; + + [[nodiscard]] static constexpr std::size_t default_accept_aare_size() noexcept { + return 76U; + } + + [[nodiscard]] static wire::EncodeResult encode_default_accept_aare_into( + std::span destination) noexcept; + + [[nodiscard]] static std::optional accept_response_size( + const AssociationRequestView& request) noexcept; + + [[nodiscard]] static wire::EncodeResult build_accept_response_into( + const AssociationRequestView& request, + std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult build_accept_response_into( + const AssociationRequestView& request, + std::span mms_initiate_response, + std::span destination) noexcept; +}; + +} // namespace ar::iec61850::acse diff --git a/include/ariec61850/asn1/ber_span_reader.hpp b/include/ariec61850/asn1/ber_span_reader.hpp new file mode 100644 index 000000000..82b58965b --- /dev/null +++ b/include/ariec61850/asn1/ber_span_reader.hpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/asn1/ber_types.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::asn1 { + +struct BerTlvView final { + std::uint8_t encoded_tag{}; + BerClass tag_class{BerClass::universal}; + bool constructed{}; + std::int32_t tag_number{}; + std::size_t header_bytes{}; + std::size_t encoded_bytes{}; + std::span value{}; +}; + +class BerSpanReader final { +public: + // Read one definite-length BER TLV without allocating or copying its value. + // The input offset is advanced only on success. + [[nodiscard]] static bool try_read_tlv( + std::span source, + std::size_t& offset, + BerTlvView& tlv) noexcept; + + [[nodiscard]] static bool try_read_exact( + std::span source, + BerTlvView& tlv) noexcept; + + [[nodiscard]] static std::optional read_unsigned_integer( + const BerTlvView& tlv) noexcept; + + [[nodiscard]] static std::optional read_signed_integer( + const BerTlvView& tlv) noexcept; + + [[nodiscard]] static std::optional read_uint32( + const BerTlvView& tlv) noexcept; +}; + +} // namespace ar::iec61850::asn1 diff --git a/include/ariec61850/asn1/ber_span_writer.hpp b/include/ariec61850/asn1/ber_span_writer.hpp new file mode 100644 index 000000000..f169ea7e5 --- /dev/null +++ b/include/ariec61850/asn1/ber_span_writer.hpp @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/asn1/ber_types.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::asn1 { + +class BerSpanWriter final { +public: + explicit BerSpanWriter(std::span destination) noexcept + : destination_{destination} {} + + [[nodiscard]] static std::optional identifier_octets( + const std::int32_t tag_number) noexcept { + if (tag_number < 0) { + return std::nullopt; + } + if (tag_number <= 30) { + return 1U; + } + + auto remaining = static_cast(tag_number); + std::size_t groups = 0U; + do { + ++groups; + remaining >>= 7U; + } while (remaining != 0U); + return 1U + groups; + } + + [[nodiscard]] static std::optional length_octets( + const std::size_t value_length) noexcept { + if (value_length < 0x80U) { + return 1U; + } + if (value_length > std::numeric_limits::max()) { + return std::nullopt; + } + + auto value = static_cast(value_length); + std::size_t bytes = 0U; + while (value != 0U) { + ++bytes; + value >>= 8U; + } + return 1U + bytes; + } + + [[nodiscard]] static std::optional tlv_size( + const std::int32_t tag_number, + const std::size_t value_length) noexcept { + const auto identifier = identifier_octets(tag_number); + const auto length = length_octets(value_length); + if (!identifier || !length) { + return std::nullopt; + } + if (*length > std::numeric_limits::max() - *identifier) { + return std::nullopt; + } + const auto header = *identifier + *length; + if (value_length > std::numeric_limits::max() - header) { + return std::nullopt; + } + return header + value_length; + } + + [[nodiscard]] bool write_identifier( + const BerClass tag_class, + const bool constructed, + const std::int32_t tag_number) noexcept { + if (tag_number < 0) { + failed_ = true; + return false; + } + + const auto class_bits = static_cast( + static_cast(tag_class) << 6U); + const auto constructed_bit = constructed ? std::uint8_t{0x20U} : std::uint8_t{0U}; + if (tag_number <= 30) { + return write_byte(static_cast( + class_bits | constructed_bit | static_cast(tag_number))); + } + + if (!write_byte(static_cast(class_bits | constructed_bit | 0x1FU))) { + return false; + } + + std::array groups{}; + std::size_t count = 0U; + auto remaining = static_cast(tag_number); + do { + if (count >= groups.size()) { + failed_ = true; + return false; + } + groups[count++] = static_cast(remaining & 0x7FU); + remaining >>= 7U; + } while (remaining != 0U); + + for (std::size_t index = count; index-- > 0U;) { + auto byte = groups[index]; + if (index != 0U) { + byte = static_cast(byte | 0x80U); + } + if (!write_byte(byte)) { + return false; + } + } + return true; + } + + [[nodiscard]] bool write_length(const std::size_t value_length) noexcept { + if (value_length < 0x80U) { + return write_byte(static_cast(value_length)); + } + if (value_length > std::numeric_limits::max()) { + failed_ = true; + return false; + } + + std::array bytes{}; + auto value = static_cast(value_length); + std::size_t count = 0U; + while (value != 0U) { + bytes[bytes.size() - 1U - count] = static_cast(value & 0xFFU); + value >>= 8U; + ++count; + } + if (!write_byte(static_cast(0x80U | count))) { + return false; + } + return write_bytes(std::span{bytes}.last(count)); + } + + [[nodiscard]] bool write_tlv_header( + const BerClass tag_class, + const bool constructed, + const std::int32_t tag_number, + const std::size_t value_length) noexcept { + return write_identifier(tag_class, constructed, tag_number) && + write_length(value_length); + } + + [[nodiscard]] bool write_tlv( + const BerClass tag_class, + const bool constructed, + const std::int32_t tag_number, + const std::span value) noexcept { + return write_tlv_header(tag_class, constructed, tag_number, value.size()) && + write_bytes(value); + } + + [[nodiscard]] bool write_bytes(const std::span value) noexcept { + if (failed_ || value.size() > remaining()) { + failed_ = true; + return false; + } + std::copy(value.begin(), value.end(), destination_.begin() + + static_cast(offset_)); + offset_ += value.size(); + return true; + } + + [[nodiscard]] bool write_byte(const std::uint8_t value) noexcept { + if (failed_ || offset_ >= destination_.size()) { + failed_ = true; + return false; + } + destination_[offset_++] = value; + return true; + } + + [[nodiscard]] std::size_t size() const noexcept { return offset_; } + [[nodiscard]] std::size_t remaining() const noexcept { + return offset_ <= destination_.size() ? destination_.size() - offset_ : 0U; + } + [[nodiscard]] bool good() const noexcept { return !failed_; } + +private: + std::span destination_; + std::size_t offset_{}; + bool failed_{}; +}; + +} // namespace ar::iec61850::asn1 diff --git a/include/ariec61850/asn1/ber_types.hpp b/include/ariec61850/asn1/ber_types.hpp new file mode 100644 index 000000000..b4b3a5c4a --- /dev/null +++ b/include/ariec61850/asn1/ber_types.hpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +namespace ar::iec61850::asn1 { + +enum class BerClass : std::uint8_t { + universal = 0, + application = 1, + context_specific = 2, + private_class = 3 +}; + +} // namespace ar::iec61850::asn1 diff --git a/include/ariec61850/mms/buffered_selective_information_report_span.hpp b/include/ariec61850/mms/buffered_selective_information_report_span.hpp new file mode 100644 index 000000000..cbbdcfbfd --- /dev/null +++ b/include/ariec61850/mms/buffered_selective_information_report_span.hpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/information_report_span.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +// Selective InformationReport form for buffered reports. EntryID and BufOvfl +// are encoded only when selected by OptFlds. The DataSet inclusion bit-string +// keeps the original DataSet cardinality while values/reasons are emitted only +// for selected members. +struct MmsBufferedSelectiveInformationReportSnapshotInput final { + std::string_view report_id; + std::span optional_fields; + std::uint32_t sequence_number{}; + std::span report_time; + MmsInformationReportReferenceInput data_set_reference{}; + bool buffer_overflow{}; + std::span entry_id; + std::uint32_t conf_revision{}; + + std::size_t data_set_member_count{}; + std::span included_member_indices; + std::span included_member_references; + std::span included_member_results; + std::span included_reason_for_inclusion; +}; + +class MmsBufferedSelectiveInformationReportSpanCodec final { +public: + [[nodiscard]] static wire::EncodeResult encode_snapshot_into( + const MmsBufferedSelectiveInformationReportSnapshotInput& report, + std::span destination) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/buffered_selective_report_detail.hpp b/include/ariec61850/mms/buffered_selective_report_detail.hpp new file mode 100644 index 000000000..774b2ab62 --- /dev/null +++ b/include/ariec61850/mms/buffered_selective_report_detail.hpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" +#include "ariec61850/mms/buffered_selective_information_report_span.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms::detail { + +inline constexpr std::uint8_t opt_sequence_number = 0x40U; +inline constexpr std::uint8_t opt_report_timestamp = 0x20U; +inline constexpr std::uint8_t opt_reason_for_inclusion = 0x10U; +inline constexpr std::uint8_t opt_data_set_name = 0x08U; +inline constexpr std::uint8_t opt_data_reference = 0x04U; +inline constexpr std::uint8_t opt_buffer_overflow = 0x02U; +inline constexpr std::uint8_t opt_entry_id = 0x01U; +inline constexpr std::uint8_t opt_conf_revision = 0x80U; + +[[nodiscard]] inline bool optional_selected( + const MmsBufferedSelectiveInformationReportSnapshotInput& report, + const std::size_t byte_index, + const std::uint8_t mask) noexcept { + return report.optional_fields.size() > byte_index && + (report.optional_fields[byte_index] & mask) != 0U; +} + +[[nodiscard]] inline bool visible_ascii( + const std::string_view text, + const std::size_t maximum_bytes) noexcept { + if (text.empty() || text.size() > maximum_bytes) return false; + for (const auto character : text) { + const auto byte = static_cast(character); + if (byte == 0U || byte > 0x7FU) return false; + } + return true; +} + +[[nodiscard]] inline std::size_t minimal_unsigned_size(std::uint32_t value) noexcept { + std::size_t bytes = 1U; + while (value > 0xFFU) { + ++bytes; + value >>= 8U; + } + return bytes; +} + +[[nodiscard]] inline bool needs_positive_prefix( + const std::uint32_t value, + const std::size_t minimal_bytes) noexcept { + const auto shift = static_cast((minimal_bytes - 1U) * 8U); + return ((value >> shift) & 0x80U) != 0U; +} + +[[nodiscard]] inline std::size_t positive_integer_size(const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + return minimal + (needs_positive_prefix(value, minimal) ? 1U : 0U); +} + +[[nodiscard]] inline bool write_unsigned_content( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto bytes = minimal_unsigned_size(value); + for (std::size_t index = bytes; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] inline bool write_positive_content( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto bytes = minimal_unsigned_size(value); + if (needs_positive_prefix(value, bytes) && !writer.write_byte(0x00U)) return false; + return write_unsigned_content(writer, value); +} + +[[nodiscard]] inline bool valid_mms_data(const std::span encoded) noexcept { + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific) return false; + if (tlv.tag_number == 1 || tlv.tag_number == 2) return tlv.constructed; + return tlv.tag_number >= 3 && tlv.tag_number <= 17 && !tlv.constructed; +} + +[[nodiscard]] inline std::optional access_result_size( + const MmsReadAccessResultInput& result) noexcept { + if (result.success) { + return valid_mms_data(result.encoded_data) + ? std::optional{result.encoded_data.size()} + : std::nullopt; + } + return asn1::BerSpanWriter::tlv_size(0, positive_integer_size(result.failure_code)); +} + +[[nodiscard]] inline bool write_access_result( + asn1::BerSpanWriter& writer, + const MmsReadAccessResultInput& result) noexcept { + if (result.success) return writer.write_bytes(result.encoded_data); + const auto content = positive_integer_size(result.failure_code); + return writer.write_tlv_header(asn1::BerClass::context_specific, false, 0, content) && + write_positive_content(writer, result.failure_code); +} + +[[nodiscard]] inline std::optional reference_content_size( + const MmsInformationReportReferenceInput& reference) noexcept { + if (!visible_ascii(reference.domain, MmsServiceSpanCodec::maximum_identifier_bytes) || + !visible_ascii(reference.item, MmsServiceSpanCodec::maximum_identifier_bytes)) { + return std::nullopt; + } + if (reference.item.size() > MmsInformationReportSpanCodec::maximum_reference_bytes - 1U || + reference.domain.size() > + MmsInformationReportSpanCodec::maximum_reference_bytes - 1U - reference.item.size()) { + return std::nullopt; + } + return reference.domain.size() + 1U + reference.item.size(); +} + +[[nodiscard]] inline bool write_reference( + asn1::BerSpanWriter& writer, + const MmsInformationReportReferenceInput& reference) noexcept { + const auto content = reference_content_size(reference); + return content && + writer.write_tlv_header(asn1::BerClass::context_specific, false, 10, *content) && + writer.write_bytes(std::span{ + reinterpret_cast(reference.domain.data()), + reference.domain.size()}) && + writer.write_byte(static_cast('/')) && + writer.write_bytes(std::span{ + reinterpret_cast(reference.item.data()), + reference.item.size()}); +} + +[[nodiscard]] inline bool write_visible( + asn1::BerSpanWriter& writer, + const std::string_view text) noexcept { + return writer.write_tlv_header(asn1::BerClass::context_specific, false, 10, text.size()) && + writer.write_bytes(std::span{ + reinterpret_cast(text.data()), text.size()}); +} + +[[nodiscard]] inline bool write_bit_string( + asn1::BerSpanWriter& writer, + const std::uint8_t unused_bits, + const std::span bytes) noexcept { + return unused_bits <= 7U && + writer.write_tlv_header(asn1::BerClass::context_specific, false, 4, 1U + bytes.size()) && + writer.write_byte(unused_bits) && writer.write_bytes(bytes); +} + +[[nodiscard]] inline bool write_unsigned( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto content = minimal_unsigned_size(value); + return writer.write_tlv_header(asn1::BerClass::context_specific, false, 6, content) && + write_unsigned_content(writer, value); +} + +[[nodiscard]] inline bool write_boolean( + asn1::BerSpanWriter& writer, + const bool value) noexcept { + return writer.write_tlv_header(asn1::BerClass::context_specific, false, 3, 1U) && + writer.write_byte(value ? 0x01U : 0x00U); +} + +[[nodiscard]] inline bool add_to( + std::size_t& total, + const std::optional value) noexcept { + if (!value || *value > std::numeric_limits::max() - total) return false; + total += *value; + return true; +} + +} // namespace ar::iec61850::mms::detail diff --git a/include/ariec61850/mms/connection_runtime.hpp b/include/ariec61850/mms/connection_runtime.hpp new file mode 100644 index 000000000..294ceea17 --- /dev/null +++ b/include/ariec61850/mms/connection_runtime.hpp @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_dispatcher.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticConnectionState : std::uint8_t { + awaiting_cotp_connect, + awaiting_association, + established, + closed, + fault, +}; + +enum class MmsStaticConnectionStatus : std::uint8_t { + need_more, + response_ready, + consumed_no_response, + malformed_transport, + protocol_violation, + application_rejected, + response_buffer_too_small, + workspace_too_small, + peer_limit_exceeded, + backend_failure, + closed, +}; + +using MmsStaticConnectionNowMsCallback = std::uint64_t (*)( + const void* context) noexcept; + +using MmsStaticAssociationClosedCallback = void (*)( + void* context, + std::uint64_t association_id, + std::uint64_t now_ms) noexcept; + +struct MmsStaticConnectionPolicy final { + static constexpr std::size_t maximum_owner_bytes = 16U; + + std::uint16_t cotp_source_reference{0x1001U}; + std::uint8_t maximum_tpdu_size_code{0x0AU}; + bool require_end_of_transmission{true}; + + // Optional portable association identity used by contextual MMS writes. + // The transport/server adapter assigns association_id and stable Owner. + // Leaving these zero/empty preserves legacy non-contextual behavior. + std::uint64_t association_id{}; + std::array owner{}; + std::size_t owner_size{}; + + // Optional lifecycle bridge. The connection runtime remains independent of + // reporting/control classes: a server adapter may forward this event to a + // BRCB control block, session registry, audit sink, or several fan-out + // targets. `now_ms` must use the same monotonic clock as ResvTms handling. + MmsStaticConnectionNowMsCallback now_ms{}; + const void* now_context{}; + MmsStaticAssociationClosedCallback association_closed{}; + void* association_closed_context{}; + + // Appended for aggregate source compatibility. When non-empty, this is the + // adapter-owned COTP Data reassembly buffer. Otherwise the runtime may + // borrow half of the stable per-session workspace while a fragmented COTP + // message is active. No heap/global storage is introduced. + std::span cotp_reassembly{}; + + [[nodiscard]] constexpr MmsStaticRequestAccessContext access_context() const noexcept { + return { + association_id, + owner_size > 0U && owner_size <= owner.size() + ? std::span{owner.data(), owner_size} + : std::span{}}; + } +}; + +struct MmsStaticConnectionResult final { + MmsStaticConnectionStatus status{MmsStaticConnectionStatus::need_more}; + MmsStaticConnectionState state{MmsStaticConnectionState::awaiting_cotp_connect}; + std::size_t consumed_bytes{}; + std::size_t bytes_written{}; + std::size_t required_response_bytes{}; + std::size_t required_workspace_bytes{}; + MmsStaticDispatchStatus application_status{MmsStaticDispatchStatus::malformed_request}; + MmsWireConfirmedService application_service{MmsWireConfirmedService::unknown}; + std::uint32_t invoke_id{}; + + [[nodiscard]] constexpr bool response_ready() const noexcept { + return status == MmsStaticConnectionStatus::response_ready; + } +}; + +class MmsStaticConnectionRuntime final { +public: + explicit constexpr MmsStaticConnectionRuntime( + const MmsStaticApplicationDispatcher& dispatcher, + const MmsStaticConnectionPolicy policy = {}) noexcept + : dispatcher_{dispatcher}, policy_{policy} {} + + // Process at most the first complete TPKT frame in a TCP receive window. + // `consumed_bytes` identifies the prefix the transport adapter may remove. + // On need_more or buffer-capacity failures no input is consumed, allowing + // the same frame to be retried once more bytes/capacity are available. + [[nodiscard]] MmsStaticConnectionResult process_tcp_window( + std::span tcp_bytes, + std::span response, + std::span workspace) noexcept; + + // Notify the lifecycle bridge exactly once for an established association + // and mark the transport closed. Call this for TCP EOF, socket error, local + // close, or any fatal transport teardown not represented by a COTP DR TPDU. + void close_transport() noexcept; + + // Reuse this runtime for a fresh transport. If an association was still + // active, reset first emits the same single association-closed event. + void reset() noexcept; + + [[nodiscard]] constexpr MmsStaticConnectionState state() const noexcept { + return state_; + } + + [[nodiscard]] constexpr std::uint32_t mms_presentation_context_id() const noexcept { + return mms_presentation_context_id_; + } + + // Negotiated outbound limits. They are zero until the corresponding COTP + // and MMS association handshakes have completed successfully. + [[nodiscard]] constexpr std::size_t negotiated_tpdu_size_bytes() const noexcept { + return negotiated_tpdu_size_bytes_; + } + + [[nodiscard]] constexpr std::uint32_t negotiated_mms_pdu_size() const noexcept { + return negotiated_mms_pdu_size_; + } + + [[nodiscard]] constexpr MmsStaticRequestAccessContext access_context() const noexcept { + return policy_.access_context(); + } + + [[nodiscard]] constexpr bool association_active() const noexcept { + return association_active_; + } + +private: + void notify_association_closed() noexcept; + void clear_cotp_reassembly() noexcept; + + const MmsStaticApplicationDispatcher& dispatcher_; + MmsStaticConnectionPolicy policy_{}; + MmsStaticConnectionState state_{MmsStaticConnectionState::awaiting_cotp_connect}; + std::uint32_t mms_presentation_context_id_{}; + std::size_t negotiated_tpdu_size_bytes_{}; + std::uint32_t negotiated_mms_pdu_size_{}; + std::size_t cotp_reassembly_size_{}; + bool cotp_reassembly_complete_{}; + bool cotp_reassembly_uses_workspace_{}; + std::uint8_t* cotp_reassembly_workspace_data_{}; + std::size_t cotp_reassembly_workspace_size_{}; + bool association_active_{}; + bool association_close_notified_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/data_set_span.hpp b/include/ariec61850/mms/data_set_span.hpp new file mode 100644 index 000000000..fd40adbf9 --- /dev/null +++ b/include/ariec61850/mms/data_set_span.hpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +struct MmsNamedVariableListAttributesRequestView final { + std::uint32_t invoke_id{}; + MmsObjectNameView name{}; +}; + +struct MmsNamedVariableListAttributesResponseView final { + std::uint32_t invoke_id{}; + bool mms_deletable{}; + std::span variable_list{}; + std::size_t member_count{}; + + [[nodiscard]] bool try_member( + std::size_t index, + MmsObjectNameView& name) const noexcept; +}; + +struct MmsNamedVariableListMemberInput final { + std::string_view domain; + std::string_view item; +}; + +class MmsDataSetSpanCodec final { +public: + static constexpr std::size_t maximum_members = MmsServiceSpanCodec::maximum_variables; + + [[nodiscard]] static bool try_decode_get_named_variable_list_attributes_request( + const MmsConfirmedPduView& confirmed, + MmsNamedVariableListAttributesRequestView& request) noexcept; + + [[nodiscard]] static bool try_decode_get_named_variable_list_attributes_request( + std::span mms_pdu, + MmsNamedVariableListAttributesRequestView& request) noexcept; + + [[nodiscard]] static bool try_decode_get_named_variable_list_attributes_response( + const MmsConfirmedPduView& confirmed, + MmsNamedVariableListAttributesResponseView& response) noexcept; + + [[nodiscard]] static bool try_decode_get_named_variable_list_attributes_response( + std::span mms_pdu, + MmsNamedVariableListAttributesResponseView& response) noexcept; + + [[nodiscard]] static wire::EncodeResult + encode_get_named_variable_list_attributes_response_into( + std::uint32_t invoke_id, + bool mms_deletable, + std::span members, + std::span destination) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/data_span_codec.hpp b/include/ariec61850/mms/data_span_codec.hpp new file mode 100644 index 000000000..39c13690a --- /dev/null +++ b/include/ariec61850/mms/data_span_codec.hpp @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/asn1/ber_span_writer.hpp" +#include "ariec61850/mms/data_value.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +class MmsDataSpanCodec final { +public: + static constexpr std::size_t maximum_nesting_depth = 16U; + + [[nodiscard]] static std::optional encoded_size( + const MmsDataValue& value) noexcept { + return encoded_size_impl(value, 0U); + } + + [[nodiscard]] static std::optional encoded_all_size( + const std::span values) noexcept { + return encoded_all_size_impl(values, 0U); + } + + [[nodiscard]] static wire::EncodeResult encode_into( + const MmsDataValue& value, + const std::span destination) noexcept { + const auto required = encoded_size(value); + if (!required) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!write_value(writer, value, 0U) || writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, writer.size(), *required}; + } + + [[nodiscard]] static wire::EncodeResult encode_all_into( + const std::span values, + const std::span destination) noexcept { + const auto required = encoded_all_size(values); + if (!required) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + for (const auto& value : values) { + if (!write_value(writer, value, 0U)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + if (writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, writer.size(), *required}; + } + +private: + [[nodiscard]] static std::optional tag_for( + const MmsDataValue& value) noexcept { + switch (value.kind()) { + case MmsDataKind::array: return 1; + case MmsDataKind::structure: return 2; + case MmsDataKind::boolean: return 3; + case MmsDataKind::bit_string: return 4; + case MmsDataKind::integer: return 5; + case MmsDataKind::unsigned_integer: return 6; + case MmsDataKind::floating_point: return 7; + case MmsDataKind::octet_string: return 9; + case MmsDataKind::visible_string: return 10; + case MmsDataKind::binary_time: return 12; + case MmsDataKind::mms_string: return 16; + case MmsDataKind::utc_time: return 17; + case MmsDataKind::unknown: + return value.unknown_tag_number(); + default: + return std::nullopt; + } + } + + [[nodiscard]] static std::size_t unsigned_integer_size( + std::uint64_t value) noexcept { + std::size_t size = 1U; + while (value > 0xFFU) { + ++size; + value >>= 8U; + } + return size; + } + + [[nodiscard]] static std::size_t signed_integer_size( + const std::int64_t value) noexcept { + const auto raw = std::bit_cast(value); + std::array bytes{}; + for (std::size_t index = 0U; index < bytes.size(); ++index) { + const auto shift = static_cast((bytes.size() - 1U - index) * 8U); + bytes[index] = static_cast((raw >> shift) & 0xFFU); + } + + std::size_t first = 0U; + while (first < bytes.size() - 1U) { + const auto current = bytes[first]; + const auto next = bytes[first + 1U]; + const bool redundant_positive = current == 0x00U && (next & 0x80U) == 0U; + const bool redundant_negative = current == 0xFFU && (next & 0x80U) != 0U; + if (!redundant_positive && !redundant_negative) { + break; + } + ++first; + } + return bytes.size() - first; + } + + [[nodiscard]] static std::optional content_size( + const MmsDataValue& value, + const std::size_t depth) noexcept { + if (depth > maximum_nesting_depth) { + return std::nullopt; + } + + switch (value.kind()) { + case MmsDataKind::array: + case MmsDataKind::structure: + return encoded_all_size_impl(value.children(), depth + 1U); + case MmsDataKind::boolean: + return std::get_if(&value.value()) != nullptr + ? std::optional{1U} + : std::nullopt; + case MmsDataKind::bit_string: + case MmsDataKind::octet_string: + case MmsDataKind::binary_time: + return value.raw_value().size(); + case MmsDataKind::integer: { + const auto* scalar = std::get_if(&value.value()); + return scalar != nullptr + ? std::optional{signed_integer_size(*scalar)} + : std::nullopt; + } + case MmsDataKind::unsigned_integer: { + const auto* scalar = std::get_if(&value.value()); + return scalar != nullptr + ? std::optional{unsigned_integer_size(*scalar)} + : std::nullopt; + } + case MmsDataKind::floating_point: + return (std::get_if(&value.value()) != nullptr || + std::get_if(&value.value()) != nullptr) + ? std::optional{5U} + : std::nullopt; + case MmsDataKind::visible_string: + case MmsDataKind::mms_string: { + const auto* text = std::get_if(&value.value()); + return text != nullptr + ? std::optional{text->size()} + : std::nullopt; + } + case MmsDataKind::utc_time: { + const auto* utc = std::get_if(&value.value()); + if (utc == nullptr) { + return std::nullopt; + } + std::array bytes{}; + return utc->try_write_bytes(bytes) + ? std::optional{bytes.size()} + : std::nullopt; + } + case MmsDataKind::unknown: + return value.unknown_tag_number().has_value() + ? std::optional{value.raw_value().size()} + : std::nullopt; + default: + return std::nullopt; + } + } + + [[nodiscard]] static std::optional encoded_size_impl( + const MmsDataValue& value, + const std::size_t depth) noexcept { + if (depth > maximum_nesting_depth) { + return std::nullopt; + } + const auto tag = tag_for(value); + const auto content = content_size(value, depth); + if (!tag || !content) { + return std::nullopt; + } + return asn1::BerSpanWriter::tlv_size(*tag, *content); + } + + [[nodiscard]] static std::optional encoded_all_size_impl( + const std::span values, + const std::size_t depth) noexcept { + if (depth > maximum_nesting_depth) { + return std::nullopt; + } + std::size_t total = 0U; + for (const auto& value : values) { + const auto size = encoded_size_impl(value, depth); + if (!size || *size > std::numeric_limits::max() - total) { + return std::nullopt; + } + total += *size; + } + return total; + } + + [[nodiscard]] static bool write_unsigned( + asn1::BerSpanWriter& writer, + const std::uint64_t value) noexcept { + const auto size = unsigned_integer_size(value); + for (std::size_t index = size; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return false; + } + } + return true; + } + + [[nodiscard]] static bool write_signed( + asn1::BerSpanWriter& writer, + const std::int64_t value) noexcept { + const auto raw = std::bit_cast(value); + std::array bytes{}; + for (std::size_t index = 0U; index < bytes.size(); ++index) { + const auto shift = static_cast((bytes.size() - 1U - index) * 8U); + bytes[index] = static_cast((raw >> shift) & 0xFFU); + } + const auto size = signed_integer_size(value); + return writer.write_bytes(std::span{bytes}.last(size)); + } + + [[nodiscard]] static bool write_value( + asn1::BerSpanWriter& writer, + const MmsDataValue& value, + const std::size_t depth) noexcept { + if (depth > maximum_nesting_depth) { + return false; + } + const auto tag = tag_for(value); + const auto content = content_size(value, depth); + if (!tag || !content) { + return false; + } + const bool constructed = + value.kind() == MmsDataKind::array || value.kind() == MmsDataKind::structure; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, + constructed, + *tag, + *content)) { + return false; + } + + switch (value.kind()) { + case MmsDataKind::array: + case MmsDataKind::structure: + for (const auto& child : value.children()) { + if (!write_value(writer, child, depth + 1U)) { + return false; + } + } + return true; + case MmsDataKind::boolean: { + const auto* scalar = std::get_if(&value.value()); + return scalar != nullptr && writer.write_byte(*scalar ? 0x01U : 0x00U); + } + case MmsDataKind::bit_string: + case MmsDataKind::octet_string: + case MmsDataKind::binary_time: + case MmsDataKind::unknown: + return writer.write_bytes(value.raw_value()); + case MmsDataKind::integer: { + const auto* scalar = std::get_if(&value.value()); + return scalar != nullptr && write_signed(writer, *scalar); + } + case MmsDataKind::unsigned_integer: { + const auto* scalar = std::get_if(&value.value()); + return scalar != nullptr && write_unsigned(writer, *scalar); + } + case MmsDataKind::floating_point: { + float scalar = 0.0F; + if (const auto* single = std::get_if(&value.value())) { + scalar = *single; + } else if (const auto* double_value = std::get_if(&value.value())) { + scalar = static_cast(*double_value); + } else { + return false; + } + const auto raw = std::bit_cast(scalar); + return writer.write_byte(0x08U) && + writer.write_byte(static_cast((raw >> 24U) & 0xFFU)) && + writer.write_byte(static_cast((raw >> 16U) & 0xFFU)) && + writer.write_byte(static_cast((raw >> 8U) & 0xFFU)) && + writer.write_byte(static_cast(raw & 0xFFU)); + } + case MmsDataKind::visible_string: + case MmsDataKind::mms_string: { + const auto* text = std::get_if(&value.value()); + if (text == nullptr) { + return false; + } + for (const auto character : *text) { + if (!writer.write_byte(static_cast(character))) { + return false; + } + } + return true; + } + case MmsDataKind::utc_time: { + const auto* utc = std::get_if(&value.value()); + if (utc == nullptr) { + return false; + } + std::array bytes{}; + return utc->try_write_bytes(bytes) && writer.write_bytes(bytes); + } + default: + return false; + } + } +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/information_report_span.hpp b/include/ariec61850/mms/information_report_span.hpp new file mode 100644 index 000000000..c1d1ee1f6 --- /dev/null +++ b/include/ariec61850/mms/information_report_span.hpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +struct MmsInformationReportReferenceInput final { + std::string_view domain; + std::string_view item; +}; + +struct MmsInformationReportSnapshotInput final { + std::string_view report_id; + std::span optional_fields; + std::uint32_t sequence_number{}; + std::span report_time; + MmsInformationReportReferenceInput data_set_reference{}; + bool buffered{}; + bool buffer_overflow{}; + std::span entry_id; + std::uint32_t conf_revision{}; + std::span member_references; + std::span member_results; + std::uint8_t reason_for_inclusion{}; +}; + +struct MmsInformationReportView final { + MmsObjectNameView variable_list_name{}; + std::span access_result_list{}; + std::size_t item_count{}; + + [[nodiscard]] bool try_item( + std::size_t index, + MmsReadAccessResultView& result) const noexcept; +}; + +class MmsInformationReportSpanCodec final { +public: + static constexpr std::size_t maximum_members = MmsServiceSpanCodec::maximum_variables; + static constexpr std::size_t maximum_reference_bytes = 129U; + static constexpr std::size_t maximum_report_id_bytes = 129U; + static constexpr std::size_t optional_field_bytes = 2U; + static constexpr std::size_t binary_time_bytes = 6U; + static constexpr std::size_t entry_id_bytes = 8U; + static constexpr std::size_t maximum_access_results = + (3U * maximum_members) + 16U; + + [[nodiscard]] static bool try_decode_information_report( + std::span mms_pdu, + MmsInformationReportView& report) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_snapshot_into( + const MmsInformationReportSnapshotInput& report, + std::span destination) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/pdu_span.hpp b/include/ariec61850/mms/pdu_span.hpp new file mode 100644 index 000000000..94bf40ddd --- /dev/null +++ b/include/ariec61850/mms/pdu_span.hpp @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsWirePduKind : std::uint8_t { + unknown, + confirmed_request, + confirmed_response, + initiate_request, + initiate_response, +}; + +enum class MmsWireConfirmedService : std::int32_t { + unknown = -1, + get_name_list = 1, + identify = 2, + read = 4, + write = 5, + get_variable_access_attributes = 6, + get_named_variable_list_attributes = 12, + file_directory = 77, +}; + +enum class MmsConfirmedRequestRejectReason : std::uint8_t { + other = 0U, + unrecognized_service = 1U, + invalid_argument = 4U, +}; + +struct MmsInitiateDetailView final { + std::uint32_t version_number{}; + std::span parameter_support_options{}; + std::span services_supported_calling{}; +}; + +struct MmsInitiateView final { + MmsWirePduKind kind{MmsWirePduKind::unknown}; + std::uint32_t maximum_mms_pdu_size{}; + std::uint32_t maximum_outstanding_calling{}; + std::uint32_t maximum_outstanding_called{}; + std::uint32_t data_structure_nesting_level{}; + MmsInitiateDetailView detail{}; +}; + +struct MmsConfirmedPduView final { + MmsWirePduKind kind{MmsWirePduKind::unknown}; + std::uint32_t invoke_id{}; + std::int32_t service_tag{-1}; + bool service_constructed{}; + std::span service_value{}; + + [[nodiscard]] MmsWireConfirmedService service() const noexcept; +}; + +class MmsPduSpanCodec final { +public: + static constexpr std::size_t maximum_pdu_bytes = 1U * 1024U * 1024U; + static constexpr std::size_t maximum_bit_string_bytes = 256U; + static constexpr std::uint32_t maximum_invoke_id = 0x7FFF'FFFFU; + + [[nodiscard]] static bool try_decode_initiate_request_view( + std::span bytes, + MmsInitiateView& request) noexcept; + + [[nodiscard]] static bool try_decode_initiate_response_view( + std::span bytes, + MmsInitiateView& response) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_default_initiate_response_into( + std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_initiate_response_into( + std::uint32_t maximum_mms_pdu_size, + std::uint32_t maximum_outstanding_calling, + std::uint32_t maximum_outstanding_called, + std::uint32_t data_structure_nesting_level, + std::span destination) noexcept; + + [[nodiscard]] static bool try_decode_confirmed_request_view( + std::span bytes, + MmsConfirmedPduView& request) noexcept; + + [[nodiscard]] static bool try_decode_confirmed_response_view( + std::span bytes, + MmsConfirmedPduView& response) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_confirmed_request_into( + std::uint32_t invoke_id, + std::int32_t service_tag, + bool service_constructed, + std::span service_value, + std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_confirmed_response_into( + std::uint32_t invoke_id, + std::int32_t service_tag, + bool service_constructed, + std::span service_value, + std::span destination) noexcept; + + // Encode MMS RejectPDU for a syntactically valid confirmed request that the + // bounded server cannot service. The result preserves the invoke ID and uses + // rejectReason.confirmed-requestPDU so clients receive a protocol-level + // outcome instead of waiting indefinitely for a response. + [[nodiscard]] static wire::EncodeResult encode_confirmed_request_reject_into( + std::uint32_t invoke_id, + MmsConfirmedRequestRejectReason reason, + std::span destination) noexcept; + + // Compatibility shims used by the isolated IED-simulator parity profile. + // They do not change the strict current capability advertisement. The + // ConfirmedError-named shim intentionally maps to the current RejectPDU + // primitive so unsupported engineering-client probes keep the association + // alive without resurrecting unsupported service claims. + [[nodiscard]] static wire::EncodeResult encode_identify_response_into( + std::uint32_t invoke_id, + std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_confirmed_error_into( + std::uint32_t invoke_id, + std::span destination) noexcept; + + [[nodiscard]] static bool is_conclude_request( + std::span bytes) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_conclude_response_into( + std::span destination) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/services_span.hpp b/include/ariec61850/mms/services_span.hpp new file mode 100644 index 000000000..2f6d172da --- /dev/null +++ b/include/ariec61850/mms/services_span.hpp @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/pdu_span.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsNameListObjectClass : std::uint32_t { + named_variable = 0U, + named_variable_list = 2U, + domain = 9U, +}; + +enum class MmsNameScopeKind : std::uint8_t { + vmd_specific, + domain_specific, + aa_specific, +}; + +enum class MmsObjectNameViewKind : std::uint8_t { + vmd_specific, + domain_specific, + aa_specific, +}; + +struct MmsObjectNameView final { + MmsObjectNameViewKind kind{MmsObjectNameViewKind::domain_specific}; + std::span domain{}; + std::span item{}; +}; + +struct MmsGetNameListRequestView final { + std::uint32_t invoke_id{}; + MmsNameListObjectClass object_class{MmsNameListObjectClass::domain}; + MmsNameScopeKind scope{MmsNameScopeKind::vmd_specific}; + std::span domain_id{}; + std::span continue_after{}; +}; + +struct MmsGetNameListResponseView final { + std::uint32_t invoke_id{}; + std::span identifier_list{}; + std::size_t identifier_count{}; + bool more_follows{}; + + [[nodiscard]] bool try_identifier( + std::size_t index, + std::span& identifier) const noexcept; +}; + +struct MmsVariableAccessAttributesRequestView final { + std::uint32_t invoke_id{}; + MmsObjectNameView name{}; +}; + +struct MmsVariableAccessAttributesResponseView final { + std::uint32_t invoke_id{}; + bool mms_deletable{}; + std::span type_specification{}; +}; + +struct MmsReadRequestView final { + std::uint32_t invoke_id{}; + bool specification_with_result{}; + std::span variable_list{}; + std::size_t variable_count{}; + + [[nodiscard]] bool try_variable( + std::size_t index, + MmsObjectNameView& name) const noexcept; +}; + +struct MmsReadAccessResultView final { + bool success{}; + std::span encoded_data{}; + std::uint32_t failure_code{}; +}; + +struct MmsReadResponseView final { + std::uint32_t invoke_id{}; + std::span access_result_list{}; + std::size_t result_count{}; + + [[nodiscard]] bool try_result( + std::size_t index, + MmsReadAccessResultView& result) const noexcept; +}; + +struct MmsReadAccessResultInput final { + bool success{}; + std::span encoded_data{}; + std::uint32_t failure_code{}; +}; + +struct MmsWriteRequestView final { + std::uint32_t invoke_id{}; + std::span variable_list{}; + std::size_t variable_count{}; + std::span data_list{}; + std::size_t data_count{}; + + [[nodiscard]] bool try_variable( + std::size_t index, + MmsObjectNameView& name) const noexcept; + + [[nodiscard]] bool try_value( + std::size_t index, + std::span& encoded_data) const noexcept; +}; + +struct MmsWriteAccessResultView final { + bool success{}; + std::uint32_t failure_code{}; +}; + +struct MmsWriteResponseView final { + std::uint32_t invoke_id{}; + std::span result_list{}; + std::size_t result_count{}; + bool implicit_single_success{}; + + [[nodiscard]] bool try_result( + std::size_t index, + MmsWriteAccessResultView& result) const noexcept; +}; + +struct MmsWriteAccessResultInput final { + bool success{}; + std::uint32_t failure_code{}; +}; + +class MmsServiceSpanCodec final { +public: + static constexpr std::size_t maximum_identifier_bytes = 1'024U; + static constexpr std::size_t maximum_identifiers = 128U; + static constexpr std::size_t maximum_variables = 64U; + + [[nodiscard]] static bool try_decode_object_name_view( + std::span encoded_object_name, + MmsObjectNameView& name) noexcept; + + [[nodiscard]] static bool try_decode_get_name_list_request( + const MmsConfirmedPduView& confirmed, + MmsGetNameListRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_get_name_list_request( + std::span mms_pdu, + MmsGetNameListRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_get_name_list_response( + const MmsConfirmedPduView& confirmed, + MmsGetNameListResponseView& response) noexcept; + [[nodiscard]] static bool try_decode_get_name_list_response( + std::span mms_pdu, + MmsGetNameListResponseView& response) noexcept; + [[nodiscard]] static wire::EncodeResult encode_get_name_list_response_into( + std::uint32_t invoke_id, + std::span names, + bool more_follows, + std::span destination) noexcept; + + [[nodiscard]] static bool try_decode_variable_access_attributes_request( + const MmsConfirmedPduView& confirmed, + MmsVariableAccessAttributesRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_variable_access_attributes_request( + std::span mms_pdu, + MmsVariableAccessAttributesRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_variable_access_attributes_response( + const MmsConfirmedPduView& confirmed, + MmsVariableAccessAttributesResponseView& response) noexcept; + [[nodiscard]] static bool try_decode_variable_access_attributes_response( + std::span mms_pdu, + MmsVariableAccessAttributesResponseView& response) noexcept; + [[nodiscard]] static wire::EncodeResult encode_variable_access_attributes_response_into( + std::uint32_t invoke_id, + bool mms_deletable, + std::span encoded_type_specification, + std::span destination) noexcept; + + [[nodiscard]] static bool try_decode_read_request( + const MmsConfirmedPduView& confirmed, + MmsReadRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_read_request( + std::span mms_pdu, + MmsReadRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_read_response( + const MmsConfirmedPduView& confirmed, + MmsReadResponseView& response) noexcept; + [[nodiscard]] static bool try_decode_read_response( + std::span mms_pdu, + MmsReadResponseView& response) noexcept; + [[nodiscard]] static wire::EncodeResult encode_read_response_into( + std::uint32_t invoke_id, + std::span results, + std::span destination) noexcept; + + [[nodiscard]] static bool try_decode_write_request( + const MmsConfirmedPduView& confirmed, + MmsWriteRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_write_request( + std::span mms_pdu, + MmsWriteRequestView& request) noexcept; + [[nodiscard]] static bool try_decode_write_response( + const MmsConfirmedPduView& confirmed, + MmsWriteResponseView& response) noexcept; + [[nodiscard]] static bool try_decode_write_response( + std::span mms_pdu, + MmsWriteResponseView& response) noexcept; + [[nodiscard]] static wire::EncodeResult encode_write_response_into( + std::uint32_t invoke_id, + std::span results, + std::span destination) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_brcb_checkpoint_store.hpp b/include/ariec61850/mms/static_brcb_checkpoint_store.hpp new file mode 100644 index 000000000..3c8cd80ac --- /dev/null +++ b/include/ariec61850/mms/static_brcb_checkpoint_store.hpp @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_brcb_state_codec.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +class MmsStaticBrcbRuntime; + +struct MmsStaticBrcbStorageBackend final { + using ReadFn = bool (*)( + void* context, + std::size_t offset, + std::span destination) noexcept; + using WriteFn = bool (*)( + void* context, + std::size_t offset, + std::span source) noexcept; + using EraseFn = bool (*)( + void* context, + std::size_t offset, + std::size_t bytes) noexcept; + using SyncFn = bool (*)(void* context) noexcept; + + void* context{}; + std::size_t capacity_bytes{}; + ReadFn read{}; + WriteFn write{}; + EraseFn erase{}; + SyncFn sync{}; + + [[nodiscard]] constexpr bool valid() const noexcept { + return capacity_bytes != 0U && read != nullptr && write != nullptr && + erase != nullptr && sync != nullptr; + } +}; + +enum class MmsStaticBrcbCheckpointStatus : std::uint8_t { + ok, + no_checkpoint, + invalid_backend, + invalid_layout, + state_buffer_too_small, + storage_failure, + state_encode_failed, + state_restore_failed, +}; + +struct MmsStaticBrcbCheckpointResult final { + MmsStaticBrcbCheckpointStatus status{ + MmsStaticBrcbCheckpointStatus::invalid_backend}; + MmsStaticBrcbStateStatus state_status{MmsStaticBrcbStateStatus::ok}; + std::uint64_t generation{}; + std::size_t bank_index{}; + std::size_t state_bytes{}; + std::size_t required_state_bytes{}; + + [[nodiscard]] constexpr bool success() const noexcept { + return status == MmsStaticBrcbCheckpointStatus::ok; + } +}; + +// Two fixed banks are used as an A/B journal. The inactive bank is erased and +// rewritten first; its commit footer is written only after the complete state +// image has been written and synchronized. The previous committed bank is kept +// intact as the recovery fallback. +class MmsStaticBrcbCheckpointStore final { +public: + static constexpr std::uint16_t format_version = 1U; + static constexpr std::size_t bank_count = 2U; + static constexpr std::size_t record_header_bytes = 40U; + static constexpr std::size_t record_footer_bytes = 32U; + + MmsStaticBrcbCheckpointStore( + MmsStaticBrcbStorageBackend backend, + std::size_t bank_bytes) noexcept + : backend_{backend}, bank_bytes_{bank_bytes} {} + + [[nodiscard]] bool valid() const noexcept; + [[nodiscard]] constexpr std::size_t bank_bytes() const noexcept { + return bank_bytes_; + } + [[nodiscard]] constexpr std::size_t maximum_state_bytes() const noexcept { + return bank_bytes_ >= record_header_bytes + record_footer_bytes + ? bank_bytes_ - record_header_bytes - record_footer_bytes + : 0U; + } + + [[nodiscard]] MmsStaticBrcbCheckpointResult checkpoint( + const MmsStaticBrcbRuntime& runtime, + std::span state_buffer) const noexcept; + + [[nodiscard]] MmsStaticBrcbCheckpointResult restore( + MmsStaticBrcbRuntime& runtime, + std::span state_buffer) const noexcept; + +private: + MmsStaticBrcbStorageBackend backend_{}; + std::size_t bank_bytes_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_brcb_connection.hpp b/include/ariec61850/mms/static_brcb_connection.hpp new file mode 100644 index 000000000..ffea1368c --- /dev/null +++ b/include/ariec61850/mms/static_brcb_connection.hpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/connection_runtime.hpp" +#include "ariec61850/mms/static_brcb_control.hpp" +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticBrcbConnectionStatus : std::uint8_t { + no_report_available, + reporting_disabled, + response_ready, + not_established, + access_denied, + response_buffer_too_small, + workspace_too_small, + peer_limit_exceeded, + frame_encode_failed, + stale_entry, +}; + +struct MmsStaticBrcbConnectionResult final { + MmsStaticBrcbConnectionStatus status{ + MmsStaticBrcbConnectionStatus::no_report_available}; + std::size_t bytes_written{}; + std::size_t required_response_bytes{}; + std::size_t required_workspace_bytes{}; + std::array entry_id{}; + std::uint8_t sequence_number{}; + bool buffer_overflow{}; + + [[nodiscard]] constexpr bool response_ready() const noexcept { + return status == MmsStaticBrcbConnectionStatus::response_ready; + } +}; + +class MmsStaticBrcbConnection final { +public: + // Stage at most one already-buffered report for the live BRCB owner. + // Reporting must be enabled through `control`, and the established MMS + // connection's association/Owner identity must exactly match the current + // live reservation. Foreign associations are denied without exposing or + // advancing retained report state. + // + // A successful poll only stages a complete TPKT image. It deliberately does + // NOT advance the BRCB delivery cursor. The transport adapter must call + // commit_sent() only after the complete staged frame has been accepted by + // the transport. Capacity errors, peer-limit rejection, denied access, + // disabled reporting, disconnected sessions and send failures therefore + // preserve the same EntryID for retry/reconnect. + [[nodiscard]] static MmsStaticBrcbConnectionResult poll( + const MmsStaticConnectionRuntime& connection, + MmsStaticBrcbControl& control, + MmsStaticBrcbRuntime& reports, + std::uint64_t now_ms, + std::span response, + std::span workspace) noexcept; + + // Second phase of report delivery. Call only after every byte of the staged + // response was accepted by the transport. The staged EntryID acts as the + // bounded delivery token. Association/Owner authorization is checked again + // at commit time so a token staged by an earlier association cannot advance + // the cursor after ownership changes. + [[nodiscard]] static bool commit_sent( + const MmsStaticConnectionRuntime& connection, + MmsStaticBrcbControl& control, + MmsStaticBrcbRuntime& reports, + std::uint64_t now_ms, + const MmsStaticBrcbConnectionResult& staged) noexcept; +}; + +} // namespace ar::iec61850::mms \ No newline at end of file diff --git a/include/ariec61850/mms/static_brcb_control.hpp b/include/ariec61850/mms/static_brcb_control.hpp new file mode 100644 index 000000000..946e8b884 --- /dev/null +++ b/include/ariec61850/mms/static_brcb_control.hpp @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticBrcbControlStatus : std::uint8_t { + ok, + invalid_runtime, + invalid_identity, + object_access_denied, + temporarily_unavailable, + entry_not_found, + backend_failure, +}; + +// Association identity is deliberately separate from the stable Owner identity. +// `association_id` is unique for one live MMS association. `owner` is a stable, +// caller-supplied binary identity that may be reused by a reconnecting client. +// No IP/socket dependency is embedded in the portable core. +struct MmsStaticBrcbClientIdentity final { + static constexpr std::size_t maximum_owner_bytes = 16U; + + std::uint64_t association_id{}; + std::array owner{}; + std::size_t owner_size{}; + + [[nodiscard]] constexpr bool valid() const noexcept { + return association_id != 0U && owner_size > 0U && owner_size <= owner.size(); + } + + [[nodiscard]] constexpr std::span owner_view() const noexcept { + return {owner.data(), owner_size <= owner.size() ? owner_size : 0U}; + } +}; + +struct MmsStaticBrcbControlStateView final { + bool reserved{}; + bool owner_connected{}; + bool report_enabled{}; + std::uint32_t resv_tms_seconds{}; + std::uint64_t association_id{}; + std::uint64_t reservation_expires_at_ms{}; + std::span owner{}; +}; + +// Portable owner/reservation gate for one BRCB runtime. The runtime itself owns +// buffered report data; this class owns association-sensitive control state. +// It is allocation-free, exception-free and does not depend on TCP/IP types. +class MmsStaticBrcbControl final { +public: + explicit constexpr MmsStaticBrcbControl(MmsStaticBrcbRuntime& reports) noexcept + : reports_{&reports} {} + + [[nodiscard]] constexpr bool valid() const noexcept { + return reports_ != nullptr && reports_->valid(); + } + + [[nodiscard]] MmsStaticBrcbControlStatus reserve( + const MmsStaticBrcbClientIdentity& client, + std::uint32_t resv_tms_seconds, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus release( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus set_report_enabled( + const MmsStaticBrcbClientIdentity& client, + bool enabled, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus replay_from( + const MmsStaticBrcbClientIdentity& client, + std::span entry_id, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus resume_after( + const MmsStaticBrcbClientIdentity& client, + std::span entry_id, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus rewind_to_oldest( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStatus purge_buffer( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + + // Called when a live MMS association is lost/closed. RptEna is always + // disabled. ResvTms==0 releases immediately; non-zero retains the stable + // Owner identity until expiry and permits that Owner to reconnect early. + void on_association_closed( + std::uint64_t association_id, + std::uint64_t now_ms) noexcept; + + // Expires a disconnected retained reservation once ResvTms elapses. + void tick(std::uint64_t now_ms) noexcept; + + [[nodiscard]] MmsStaticBrcbControlStateView state( + std::uint64_t now_ms) noexcept; + +private: + [[nodiscard]] bool same_owner( + const MmsStaticBrcbClientIdentity& client) const noexcept; + [[nodiscard]] bool owns_live_association( + const MmsStaticBrcbClientIdentity& client) const noexcept; + [[nodiscard]] MmsStaticBrcbControlStatus require_control_access( + const MmsStaticBrcbClientIdentity& client, + std::uint64_t now_ms) noexcept; + [[nodiscard]] MmsStaticBrcbControlStatus map_runtime_status( + MmsStaticBrcbStatus status) const noexcept; + void clear_reservation() noexcept; + + MmsStaticBrcbRuntime* reports_{}; + std::array owner_{}; + std::size_t owner_size_{}; + std::uint64_t association_id_{}; + std::uint64_t reservation_expires_at_ms_{}; + std::uint32_t resv_tms_seconds_{}; + bool reserved_{}; + bool owner_connected_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_brcb_objects.hpp b/include/ariec61850/mms/static_brcb_objects.hpp new file mode 100644 index 000000000..97c3594b1 --- /dev/null +++ b/include/ariec61850/mms/static_brcb_objects.hpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_brcb_control.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +using MmsStaticBrcbNowMsCallback = std::uint64_t (*)(const void* context) noexcept; + +enum class MmsStaticBrcbAttribute : std::uint8_t { + report_id, + report_enabled, + data_set, + conf_revision, + purge_buffer, + entry_id, + reservation_time, + owner, +}; + +struct MmsStaticBrcbObjectContext final { + const MmsStaticBrcbDefinition* definition{}; + MmsStaticBrcbRuntime* reports{}; + MmsStaticBrcbControl* control{}; + MmsStaticBrcbAttribute attribute{MmsStaticBrcbAttribute::report_id}; + MmsStaticBrcbNowMsCallback now_ms{}; + const void* now_context{}; +}; + +// Allocation-free MMS object facade for one bounded BRCB runtime. The caller +// owns object/context/name storage. Association-sensitive writes are routed via +// MmsStaticContextualWriteCallback and therefore receive the active request's +// opaque association/Owner identity. +class MmsStaticBrcbObjectBank final { +public: + static constexpr std::size_t attributes_per_control_block = 8U; + + MmsStaticBrcbObjectBank( + const MmsStaticBrcbDefinition& definition, + MmsStaticBrcbRuntime& reports, + MmsStaticBrcbControl& control, + std::span base_objects, + std::span object_storage, + std::span context_storage, + std::span name_storage, + MmsStaticBrcbNowMsCallback now_ms = nullptr, + const void* now_context = nullptr) noexcept + : definition_{&definition}, + reports_{&reports}, + control_{&control}, + base_objects_{base_objects}, + object_storage_{object_storage}, + context_storage_{context_storage}, + name_storage_{name_storage}, + now_ms_{now_ms}, + now_context_{now_context} {} + + [[nodiscard]] bool initialize() noexcept; + + [[nodiscard]] constexpr bool valid() const noexcept { return initialized_; } + [[nodiscard]] constexpr std::size_t object_count() const noexcept { + return initialized_ ? object_count_ : 0U; + } + [[nodiscard]] constexpr std::size_t name_bytes_used() const noexcept { + return initialized_ ? name_bytes_used_ : 0U; + } + + [[nodiscard]] std::size_t required_object_capacity() const noexcept; + [[nodiscard]] constexpr std::size_t required_context_capacity() const noexcept { + return attributes_per_control_block; + } + [[nodiscard]] std::size_t required_name_bytes() const noexcept; + + [[nodiscard]] constexpr const MmsStaticObjectTable& table() const noexcept { + return table_; + } + +private: + const MmsStaticBrcbDefinition* definition_{}; + MmsStaticBrcbRuntime* reports_{}; + MmsStaticBrcbControl* control_{}; + std::span base_objects_{}; + std::span object_storage_{}; + std::span context_storage_{}; + std::span name_storage_{}; + MmsStaticBrcbNowMsCallback now_ms_{}; + const void* now_context_{}; + MmsStaticObjectTable table_{std::span{}}; + std::size_t object_count_{}; + std::size_t name_bytes_used_{}; + bool initialized_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_brcb_runtime.hpp b/include/ariec61850/mms/static_brcb_runtime.hpp new file mode 100644 index 000000000..bb43ce04e --- /dev/null +++ b/include/ariec61850/mms/static_brcb_runtime.hpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/buffered_selective_information_report_span.hpp" +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +class MmsStaticBrcbStateCodec; + +enum class MmsStaticBrcbStatus : std::uint8_t { + ok, + invalid_runtime, + invalid_definition, + member_out_of_range, + trigger_not_selected, + temporarily_unavailable, + no_report_due, + stale_plan, + data_set_not_found, + object_not_found, + workspace_too_small, + response_buffer_too_small, + slot_too_small, + backend_failure, + report_encode_failed, + entry_not_found, +}; + +enum class MmsStaticBrcbEventReason : std::uint8_t { + data_change, + quality_change, + data_update, +}; + +struct MmsStaticBrcbDefinition final { + std::string_view domain; + std::string_view item; + std::string_view report_id; + std::string_view data_set_domain; + std::string_view data_set_item; + std::uint32_t conf_revision{1U}; + std::array + optional_fields{}; + std::uint32_t buffer_time_ms{}; + std::uint8_t trigger_options{}; +}; + +struct MmsStaticBrcbSlot final { + std::span storage{}; + std::array entry_id{}; + std::size_t bytes{}; + std::uint8_t sequence_number{}; + bool buffer_overflow{}; + bool occupied{}; +}; + +struct MmsStaticBrcbPendingState final { + std::array + member_reason_masks{}; + std::uint64_t due_ms{}; + std::uint32_t revision{}; + std::size_t pending_member_count{}; + bool pending{}; +}; + +struct MmsStaticBrcbCapturePlan final { + std::uint32_t pending_revision{}; + std::uint32_t queue_revision{}; + std::uint64_t entry_number{}; + std::uint8_t sequence_number{}; + bool buffer_overflow{}; +}; + +struct MmsStaticBrcbCaptureResult final { + MmsStaticBrcbStatus status{MmsStaticBrcbStatus::invalid_runtime}; + std::size_t bytes_written{}; + std::size_t required_bytes{}; + std::size_t included_member_count{}; + std::array entry_id{}; + + [[nodiscard]] constexpr bool success() const noexcept { + return status == MmsStaticBrcbStatus::ok; + } +}; + +struct MmsStaticBrcbEntryView final { + std::span mms_pdu{}; + std::span entry_id{}; + std::uint8_t sequence_number{}; + bool buffer_overflow{}; +}; + +class MmsStaticBrcbRuntime final { +public: + MmsStaticBrcbRuntime( + const MmsStaticBrcbDefinition& definition, + MmsStaticBrcbPendingState& pending, + std::span slots, + const MmsStaticObjectTable& objects, + const MmsStaticDataSetTable& data_sets) noexcept + : definition_{&definition}, pending_{&pending}, slots_{slots}, + objects_{&objects}, data_sets_{&data_sets} {} + + [[nodiscard]] bool initialize() noexcept; + [[nodiscard]] constexpr bool valid() const noexcept { return initialized_; } + + [[nodiscard]] MmsStaticBrcbStatus set_enabled(bool enabled) noexcept; + [[nodiscard]] constexpr bool enabled() const noexcept { return enabled_; } + + [[nodiscard]] MmsStaticBrcbStatus notify( + std::size_t data_set_member_index, + MmsStaticBrcbEventReason reason, + std::uint64_t now_ms) noexcept; + + [[nodiscard]] bool next_due( + std::uint64_t now_ms, + MmsStaticBrcbCapturePlan& plan) const noexcept; + + [[nodiscard]] MmsStaticBrcbCaptureResult capture( + const MmsStaticBrcbCapturePlan& plan, + std::span report_time, + std::span encode_buffer, + std::span workspace) noexcept; + + [[nodiscard]] bool front(MmsStaticBrcbEntryView& entry) const noexcept; + + [[nodiscard]] MmsStaticBrcbStatus commit_delivery( + std::span expected_entry_id) noexcept; + + [[nodiscard]] MmsStaticBrcbStatus replay_from( + std::span entry_id) noexcept; + + [[nodiscard]] MmsStaticBrcbStatus resume_after( + std::span entry_id) noexcept; + + [[nodiscard]] MmsStaticBrcbStatus rewind_to_oldest() noexcept; + + [[nodiscard]] MmsStaticBrcbStatus purge_buffer() noexcept; + + // EntryID attribute readback: all zero when no report is buffered, + // otherwise the newest retained report EntryID. + [[nodiscard]] std::array latest_entry_id() const noexcept { + std::array empty{}; + if (!initialized_ || slots_.empty() || count_ == 0U || + count_ > slots_.size() || head_ >= slots_.size()) { + return empty; + } + const auto physical = (head_ + count_ - 1U) % slots_.size(); + const auto& slot = slots_[physical]; + return slot.occupied ? slot.entry_id : empty; + } + + [[nodiscard]] constexpr std::size_t queue_size() const noexcept { + return delivery_offset_ <= count_ ? count_ - delivery_offset_ : 0U; + } + + [[nodiscard]] constexpr std::size_t retained_size() const noexcept { return count_; } + + [[nodiscard]] constexpr std::size_t queue_capacity() const noexcept { + return slots_.size(); + } + [[nodiscard]] constexpr std::uint64_t dropped_reports() const noexcept { + return dropped_reports_; + } + + [[nodiscard]] constexpr bool replay_gap() const noexcept { return replay_gap_; } + +private: + friend class MmsStaticBrcbStateCodec; + + [[nodiscard]] bool find_entry( + std::span entry_id, + std::size_t& logical_index) const noexcept; + void clear_retained_slots() noexcept; + + const MmsStaticBrcbDefinition* definition_{}; + MmsStaticBrcbPendingState* pending_{}; + std::span slots_{}; + const MmsStaticObjectTable* objects_{}; + const MmsStaticDataSetTable* data_sets_{}; + std::size_t head_{}; + std::size_t count_{}; + std::size_t delivery_offset_{}; + std::uint64_t next_entry_number_{1U}; + std::uint64_t dropped_reports_{}; + std::uint32_t queue_revision_{1U}; + std::uint8_t sequence_number_{}; + bool replay_gap_{}; + bool enabled_{}; + bool initialized_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_brcb_state_codec.hpp b/include/ariec61850/mms/static_brcb_state_codec.hpp new file mode 100644 index 000000000..32c85ad1e --- /dev/null +++ b/include/ariec61850/mms/static_brcb_state_codec.hpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include +#include + +namespace ar::iec61850::mms { + +class MmsStaticBrcbRuntime; + +enum class MmsStaticBrcbStateStatus : std::uint8_t { + ok, + invalid_runtime, + buffer_too_small, + invalid_state, + definition_mismatch, + capacity_mismatch, +}; + +struct MmsStaticBrcbStateResult final { + MmsStaticBrcbStateStatus status{MmsStaticBrcbStateStatus::invalid_runtime}; + std::size_t bytes_written{}; + std::size_t required_bytes{}; + + [[nodiscard]] constexpr bool success() const noexcept { + return status == MmsStaticBrcbStateStatus::ok; + } +}; + +// Storage-agnostic bounded BRCB recovery image. +// +// v1 persisted only the still-undelivered suffix. v2 persists the entire +// retained replay window plus the independent delivery cursor and replay-gap +// flag. Pending BufTm coalescing, live association ownership and RptEna remain +// deliberately transient and are never restored across reboot. +class MmsStaticBrcbStateCodec final { +public: + static constexpr std::uint16_t legacy_format_version = 1U; + static constexpr std::uint16_t format_version = 2U; + + [[nodiscard]] static MmsStaticBrcbStateResult encode( + const MmsStaticBrcbRuntime& runtime, + std::span destination) noexcept; + + // Accepts both v2 and legacy v1 images. Legacy v1 restores as an + // undelivered-only queue with delivery cursor at the oldest entry. + [[nodiscard]] static MmsStaticBrcbStateResult restore( + MmsStaticBrcbRuntime& runtime, + std::span source) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_data_set_table.hpp b/include/ariec61850/mms/static_data_set_table.hpp new file mode 100644 index 000000000..e7fc7a59d --- /dev/null +++ b/include/ariec61850/mms/static_data_set_table.hpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/data_set_span.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +struct MmsStaticDataSetMember final { + std::string_view domain; + std::string_view item; +}; + +struct MmsStaticDataSetEntry final { + std::string_view domain; + std::string_view item; + std::span members; + bool mms_deletable{}; +}; + +class MmsStaticDataSetTable final { +public: + static constexpr std::size_t maximum_data_sets = 64U; + + constexpr MmsStaticDataSetTable() noexcept = default; + + explicit constexpr MmsStaticDataSetTable( + const std::span data_sets) noexcept + : data_sets_{data_sets} {} + + [[nodiscard]] bool valid() const noexcept; + + [[nodiscard]] bool valid_against( + const MmsStaticObjectTable& objects) const noexcept; + + [[nodiscard]] const MmsStaticDataSetEntry* find( + const MmsObjectNameView& name) const noexcept; + + [[nodiscard]] constexpr std::span data_sets() const noexcept { + return data_sets_; + } + +private: + std::span data_sets_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_direct_control.hpp b/include/ariec61850/mms/static_direct_control.hpp new file mode 100644 index 000000000..49f0f3279 --- /dev/null +++ b/include/ariec61850/mms/static_direct_control.hpp @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_object_table.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +struct MmsStaticDirectBooleanOperate final { + bool control_value{}; + std::uint8_t origin_category{}; + std::uint8_t control_number{}; + bool test{}; + bool synchro_check{}; + bool interlock_check{}; +}; + +struct MmsStaticDirectBooleanControlState final { + std::uint8_t value{}; + std::uint8_t last_control_number{}; + bool last_test{}; + std::size_t accepted_operations{}; + std::size_t rejected_operations{}; +}; + +using MmsStaticBooleanApplyCallback = bool (*)( + void* context, + bool value) noexcept; + +struct MmsStaticDirectBooleanControlPolicy final { + bool allow_test{true}; + bool allow_synchro_check{}; + bool allow_interlock_check{}; + std::uint32_t malformed_failure_code{7U}; // type-inconsistent + std::uint32_t invalid_value_failure_code{11U}; // object-value-invalid + std::uint32_t backend_failure_code{10U}; // object-non-existent/backend +}; + +struct MmsStaticDirectBooleanControlBinding final { + MmsStaticDirectBooleanControlState* state{}; + MmsStaticBooleanApplyCallback apply{}; + void* apply_context{}; + MmsStaticDirectBooleanControlPolicy policy{}; +}; + +// Decode one IEC 61850 Direct-with-normal-security Oper value with the exact +// ordered structure used by the live TypeSpecification exposed by this server: +// ctlVal, origin(orCat,orIdent), ctlNum, T, Test, Check. +// The parser is allocation-free and accepts only the bounded scalar shape. +[[nodiscard]] bool try_decode_static_direct_boolean_operate( + std::span encoded_data, + MmsStaticDirectBooleanOperate& decoded) noexcept; + +// Read callbacks suitable for MmsStaticObjectEntry. +[[nodiscard]] wire::EncodeResult mms_static_direct_boolean_read_state( + const void* context, + std::span destination) noexcept; + +[[nodiscard]] wire::EncodeResult mms_static_direct_normal_read_ctl_model( + const void* context, + std::span destination) noexcept; + +[[nodiscard]] wire::EncodeResult mms_static_control_read_unavailable( + const void* context, + std::span destination) noexcept; + +// Write callback suitable for the CO$...$Oper MmsStaticObjectEntry. +// Test=true is acknowledged without changing the live output state. Check bits +// are denied by default until the application explicitly enables them. +[[nodiscard]] MmsStaticWriteResult mms_static_direct_boolean_write_oper( + void* context, + std::span encoded_data) noexcept; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_dispatcher.hpp b/include/ariec61850/mms/static_dispatcher.hpp new file mode 100644 index 000000000..4aaee27f1 --- /dev/null +++ b/include/ariec61850/mms/static_dispatcher.hpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticDispatchStatus : std::uint8_t { + response_ready, + malformed_request, + unsupported_service, + unsupported_request, + object_not_found, + invalid_object_table, + workspace_too_small, + response_buffer_too_small, + backend_failure, +}; + +struct MmsStaticDispatchPolicy final { + std::size_t maximum_names_per_response{32U}; + std::size_t maximum_write_variables{1U}; + std::uint32_t missing_object_failure_code{10U}; + std::uint32_t access_denied_failure_code{3U}; + std::uint32_t backend_failure_code{10U}; + // Some live discovery clients still derive the Logical Node root from + // flattened LN$FC$DO$DA names before requesting the root TypeSpecification. + // Keep strict IED-simulator root-only discovery as the default, while + // allowing host interoperability adapters to advertise both forms. + bool advertise_flattened_child_aliases{}; +}; + +struct MmsStaticDispatchResult final { + MmsStaticDispatchStatus status{MmsStaticDispatchStatus::malformed_request}; + MmsWireConfirmedService service{MmsWireConfirmedService::unknown}; + std::uint32_t invoke_id{}; + std::size_t bytes_written{}; + std::size_t required_bytes{}; + + [[nodiscard]] constexpr bool success() const noexcept { + return status == MmsStaticDispatchStatus::response_ready; + } +}; + +class MmsStaticApplicationDispatcher final { +public: + explicit constexpr MmsStaticApplicationDispatcher( + const MmsStaticObjectTable& objects, + const MmsStaticDispatchPolicy policy = {}) noexcept + : objects_{objects}, policy_{policy} {} + + constexpr MmsStaticApplicationDispatcher( + const MmsStaticObjectTable& objects, + const MmsStaticDataSetTable& data_sets, + const MmsStaticDispatchPolicy policy = {}) noexcept + : objects_{objects}, data_sets_{data_sets}, policy_{policy} {} + + [[nodiscard]] MmsStaticDispatchResult dispatch( + std::span mms_request, + std::span response, + std::span workspace, + const MmsStaticRequestAccessContext& access = {}) const noexcept; + + [[nodiscard]] MmsStaticDispatchResult dispatch( + const MmsConfirmedPduView& request, + std::span response, + std::span workspace, + const MmsStaticRequestAccessContext& access = {}) const noexcept; + + [[nodiscard]] constexpr const MmsStaticDispatchPolicy& policy() const noexcept { + return policy_; + } + + [[nodiscard]] constexpr const MmsStaticDataSetTable& data_sets() const noexcept { + return data_sets_; + } + +private: + const MmsStaticObjectTable& objects_; + MmsStaticDataSetTable data_sets_{}; + MmsStaticDispatchPolicy policy_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_information_report.hpp b/include/ariec61850/mms/static_information_report.hpp new file mode 100644 index 000000000..8aa58978a --- /dev/null +++ b/include/ariec61850/mms/static_information_report.hpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/information_report_span.hpp" +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticInformationReportStatus : std::uint8_t { + response_ready, + invalid_object_table, + invalid_data_set_table, + data_set_not_found, + object_not_found, + workspace_too_small, + backend_failure, + invalid_report, + response_buffer_too_small, +}; + +struct MmsStaticInformationReportInput final { + std::string_view report_id; + std::span optional_fields; + std::uint32_t sequence_number{}; + std::span report_time; + bool buffered{}; + bool buffer_overflow{}; + std::span entry_id; + std::uint32_t conf_revision{}; + std::uint8_t reason_for_inclusion{}; +}; + +struct MmsStaticInformationReportEncodeResult final { + MmsStaticInformationReportStatus status{ + MmsStaticInformationReportStatus::invalid_report}; + std::size_t bytes_written{}; + std::size_t required_bytes{}; + std::size_t member_count{}; + + [[nodiscard]] constexpr bool success() const noexcept { + return status == MmsStaticInformationReportStatus::response_ready; + } +}; + +class MmsStaticInformationReportEncoder final { +public: + [[nodiscard]] static MmsStaticInformationReportEncodeResult + encode_data_set_snapshot_into( + const MmsStaticObjectTable& objects, + const MmsStaticDataSetTable& data_sets, + const MmsObjectNameView& data_set_name, + const MmsStaticInformationReportInput& report, + std::span destination, + std::span workspace) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_object_table.hpp b/include/ariec61850/mms/static_object_table.hpp new file mode 100644 index 000000000..9852feb57 --- /dev/null +++ b/include/ariec61850/mms/static_object_table.hpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +using MmsStaticReadCallback = wire::EncodeResult (*)( + const void* context, + std::span destination) noexcept; + +struct MmsStaticWriteResult final { + bool success{}; + std::uint32_t failure_code{}; +}; + +using MmsStaticWriteCallback = MmsStaticWriteResult (*)( + void* context, + std::span encoded_data) noexcept; + +// Per-request identity propagated by a connection/runtime adapter. The core +// deliberately treats Owner as opaque binary identity and does not bind it to +// an IP address, socket, TLS implementation, or operating-system handle. +struct MmsStaticRequestAccessContext final { + std::uint64_t association_id{}; + std::span owner{}; + + [[nodiscard]] constexpr bool valid() const noexcept { + return association_id != 0U && !owner.empty(); + } +}; + +using MmsStaticContextualWriteCallback = MmsStaticWriteResult (*)( + void* context, + std::span encoded_data, + const MmsStaticRequestAccessContext& access) noexcept; + +struct MmsStaticObjectEntry final { + std::string_view domain; + std::string_view item; + std::span type_specification; + MmsStaticReadCallback read{}; + const void* context{}; + bool mms_deletable{}; + MmsStaticWriteCallback write{}; + void* write_context{}; + + // Appended for source compatibility with existing aggregate initializers. + // A contextual callback takes precedence over the legacy write callback. + MmsStaticContextualWriteCallback contextual_write{}; + + [[nodiscard]] constexpr bool writable() const noexcept { + return write != nullptr || contextual_write != nullptr; + } +}; + +class MmsStaticObjectTable final { +public: + static constexpr std::size_t maximum_objects = 128U; + + explicit constexpr MmsStaticObjectTable( + const std::span objects) noexcept + : objects_{objects} {} + + [[nodiscard]] bool valid() const noexcept; + + [[nodiscard]] const MmsStaticObjectEntry* find( + const MmsObjectNameView& name) const noexcept; + + [[nodiscard]] bool try_resolve_read_request( + const MmsReadRequestView& request, + std::span resolved, + std::size_t& resolved_count) const noexcept; + + [[nodiscard]] bool try_resolve_write_request( + const MmsWriteRequestView& request, + std::span resolved, + std::size_t& resolved_count) const noexcept; + + [[nodiscard]] constexpr std::span objects() const noexcept { + return objects_; + } + +private: + std::span objects_; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_report_connection.hpp b/include/ariec61850/mms/static_report_connection.hpp new file mode 100644 index 000000000..dd37f6cc2 --- /dev/null +++ b/include/ariec61850/mms/static_report_connection.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/connection_runtime.hpp" +#include "ariec61850/mms/static_urcb_runtime.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticReportConnectionStatus : std::uint8_t { + no_report_due, + response_ready, + not_established, + response_buffer_too_small, + workspace_too_small, + report_encode_failed, + stale_plan, +}; + +struct MmsStaticReportConnectionResult final { + MmsStaticReportConnectionStatus status{MmsStaticReportConnectionStatus::no_report_due}; + MmsStaticUrcbStatus urcb_status{MmsStaticUrcbStatus::no_report_due}; + std::size_t bytes_written{}; + std::size_t required_response_bytes{}; + std::size_t required_workspace_bytes{}; + std::size_t control_block_index{MmsStaticUrcbEmissionPlan::invalid_index}; + std::uint8_t sequence_number{}; + MmsStaticUrcbReportReason reason{MmsStaticUrcbReportReason::none}; + + [[nodiscard]] constexpr bool response_ready() const noexcept { + return status == MmsStaticReportConnectionStatus::response_ready; + } +}; + +class MmsStaticReportConnection final { +public: + // Poll one due URCB report for an already-established MMS connection. + // The URCB plan is committed only after a complete TPKT frame is encoded, + // so capacity retries cannot consume GI/integrity state or skip SqNum. + [[nodiscard]] static MmsStaticReportConnectionResult poll( + const MmsStaticConnectionRuntime& connection, + MmsStaticUrcbRuntime& reports, + std::uint64_t now_ms, + std::span report_time, + std::span response, + std::span workspace) noexcept; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_server_session.hpp b/include/ariec61850/mms/static_server_session.hpp new file mode 100644 index 000000000..664a54ab4 --- /dev/null +++ b/include/ariec61850/mms/static_server_session.hpp @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/embedded/io.hpp" +#include "ariec61850/mms/connection_runtime.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticServerSessionStatus : std::uint8_t { + progressed, + response_pending, + would_block, + timed_out, + application_rejected, + peer_closed, + transport_error, + protocol_error, + receive_buffer_full, + invalid_configuration, +}; + +struct MmsStaticServerSessionResult final { + MmsStaticServerSessionStatus status{ + MmsStaticServerSessionStatus::invalid_configuration}; + MmsStaticConnectionStatus connection_status{ + MmsStaticConnectionStatus::need_more}; + std::size_t bytes_received{}; + std::size_t bytes_sent{}; + std::size_t buffered_input_bytes{}; + std::size_t pending_output_bytes{}; + + // Preserve application evidence emitted by the connection runtime. This + // lets platform adapters diagnose a standards-level Reject/ConfirmedError + // without parsing the encoded response or coupling the core to logging. + MmsStaticDispatchStatus application_status{ + MmsStaticDispatchStatus::response_ready}; + MmsWireConfirmedService application_service{ + MmsWireConfirmedService::unknown}; + std::uint32_t invoke_id{}; + + [[nodiscard]] constexpr bool terminal() const noexcept { + return status == MmsStaticServerSessionStatus::peer_closed || + status == MmsStaticServerSessionStatus::transport_error || + status == MmsStaticServerSessionStatus::protocol_error || + status == MmsStaticServerSessionStatus::receive_buffer_full || + status == MmsStaticServerSessionStatus::invalid_configuration; + } +}; + +struct MmsStaticServerSessionBuffers final { + std::span receive; + std::span response; + std::span workspace; + + [[nodiscard]] constexpr bool valid() const noexcept { + return !receive.empty() && !response.empty() && !workspace.empty(); + } +}; + +// Allocation-free bridge between a connected TCP byte stream and the portable +// MMS connection state machine. Listen/accept/close remain platform-owned. +// One instance is used per accepted connection, which lets desktop servers, +// FreeRTOS tasks and lwIP callbacks choose their own concurrency policy. +class MmsStaticServerSession final { +public: + constexpr MmsStaticServerSession( + MmsStaticConnectionRuntime& runtime, + const embedded::TcpByteStream stream, + const MmsStaticServerSessionBuffers buffers) noexcept + : runtime_{runtime}, stream_{stream}, buffers_{buffers} {} + + // Performs at most one transport send/receive or one protocol dispatch. + // Call repeatedly from the platform event/task loop. Partial sends and + // fragmented/coalesced TPKT input are retained across calls. + [[nodiscard]] MmsStaticServerSessionResult poll_once() noexcept; + + void reset() noexcept; + + [[nodiscard]] constexpr std::size_t buffered_input_bytes() const noexcept { + return receive_size_; + } + + [[nodiscard]] constexpr std::size_t pending_output_bytes() const noexcept { + return response_size_ - response_offset_; + } + +private: + [[nodiscard]] MmsStaticServerSessionResult make_result( + MmsStaticServerSessionStatus status, + MmsStaticConnectionStatus connection_status, + std::size_t bytes_received = 0U, + std::size_t bytes_sent = 0U) const noexcept; + + void consume_input(std::size_t consumed) noexcept; + + MmsStaticConnectionRuntime& runtime_; + embedded::TcpByteStream stream_{}; + MmsStaticServerSessionBuffers buffers_{}; + std::size_t receive_size_{}; + std::size_t response_offset_{}; + std::size_t response_size_{}; + bool terminal_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_urcb_objects.hpp b/include/ariec61850/mms/static_urcb_objects.hpp new file mode 100644 index 000000000..ec3f1917c --- /dev/null +++ b/include/ariec61850/mms/static_urcb_objects.hpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_object_table.hpp" +#include "ariec61850/mms/static_urcb_runtime.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { + +using MmsStaticNowMsCallback = std::uint64_t (*)(const void* context) noexcept; + +enum class MmsStaticUrcbAttribute : std::uint8_t { + report_id, + report_enabled, + reserved, + data_set, + conf_revision, + optional_fields, + buffer_time, + trigger_options, + integrity_period, + general_interrogation, + sequence_number, +}; + +struct MmsStaticUrcbObjectContext final { + MmsStaticUrcbRuntime* runtime{}; + std::size_t index{}; + MmsStaticUrcbAttribute attribute{MmsStaticUrcbAttribute::report_id}; + MmsStaticNowMsCallback now_ms{}; + const void* now_context{}; +}; + +class MmsStaticUrcbObjectBank final { +public: + static constexpr std::size_t attributes_per_control_block = 11U; + + MmsStaticUrcbObjectBank( + MmsStaticUrcbRuntime& runtime, + std::span base_objects, + std::span object_storage, + std::span context_storage, + std::span name_storage, + MmsStaticNowMsCallback now_ms = nullptr, + const void* now_context = nullptr) noexcept + : runtime_{&runtime}, + base_objects_{base_objects}, + object_storage_{object_storage}, + context_storage_{context_storage}, + name_storage_{name_storage}, + now_ms_{now_ms}, + now_context_{now_context} {} + + [[nodiscard]] bool initialize() noexcept; + + [[nodiscard]] constexpr bool valid() const noexcept { return initialized_; } + [[nodiscard]] constexpr std::size_t object_count() const noexcept { + return initialized_ ? object_count_ : 0U; + } + [[nodiscard]] constexpr std::size_t name_bytes_used() const noexcept { + return initialized_ ? name_bytes_used_ : 0U; + } + + [[nodiscard]] std::size_t required_object_capacity() const noexcept; + [[nodiscard]] std::size_t required_context_capacity() const noexcept; + [[nodiscard]] std::size_t required_name_bytes() const noexcept; + + [[nodiscard]] constexpr const MmsStaticObjectTable& table() const noexcept { + return table_; + } + +private: + MmsStaticUrcbRuntime* runtime_{}; + std::span base_objects_{}; + std::span object_storage_{}; + std::span context_storage_{}; + std::span name_storage_{}; + MmsStaticNowMsCallback now_ms_{}; + const void* now_context_{}; + MmsStaticObjectTable table_{std::span{}}; + std::size_t object_count_{}; + std::size_t name_bytes_used_{}; + bool initialized_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/static_urcb_runtime.hpp b/include/ariec61850/mms/static_urcb_runtime.hpp new file mode 100644 index 000000000..10a6a9b88 --- /dev/null +++ b/include/ariec61850/mms/static_urcb_runtime.hpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/mms/static_information_report.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { + +enum class MmsStaticUrcbStatus : std::uint8_t { + ok, + invalid_runtime, + index_out_of_range, + invalid_value, + object_access_denied, + temporarily_unavailable, + data_set_not_found, + no_report_due, + stale_plan, + workspace_too_small, + backend_failure, + response_buffer_too_small, + report_encode_failed, +}; + +enum class MmsStaticUrcbReportReason : std::uint8_t { + none = 0x00U, + general_interrogation = 0x08U, + integrity = 0x10U, +}; + +struct MmsStaticUrcbDefinition final { + std::string_view domain; + std::string_view item; + std::string_view report_id; + std::string_view data_set_domain; + std::string_view data_set_item; + std::uint32_t conf_revision{1U}; + std::array + optional_fields{}; + std::uint32_t buffer_time_ms{}; + std::uint8_t trigger_options{}; + std::uint32_t integrity_period_ms{}; +}; + +struct MmsStaticUrcbState final { + static constexpr std::size_t maximum_report_id_bytes = + MmsInformationReportSpanCodec::maximum_report_id_bytes; + static constexpr std::size_t maximum_reference_component_bytes = + MmsInformationReportSpanCodec::maximum_reference_bytes; + + std::array report_id_storage{}; + std::size_t report_id_size{}; + std::array data_set_domain_storage{}; + std::size_t data_set_domain_size{}; + std::array data_set_item_storage{}; + std::size_t data_set_item_size{}; + std::array + optional_fields{}; + std::uint32_t conf_revision{1U}; + std::uint32_t buffer_time_ms{}; + std::uint32_t integrity_period_ms{}; + std::uint64_t next_integrity_due_ms{}; + std::uint32_t revision{}; + std::uint8_t trigger_options{}; + std::uint8_t sequence_number{}; + bool enabled{}; + bool reserved{}; + bool general_interrogation_pending{}; + bool integrity_armed{}; + + [[nodiscard]] std::string_view report_id() const noexcept { + return {report_id_storage.data(), report_id_size}; + } + + [[nodiscard]] std::string_view data_set_domain() const noexcept { + return {data_set_domain_storage.data(), data_set_domain_size}; + } + + [[nodiscard]] std::string_view data_set_item() const noexcept { + return {data_set_item_storage.data(), data_set_item_size}; + } +}; + +struct MmsStaticUrcbEmissionPlan final { + static constexpr std::size_t invalid_index = + std::numeric_limits::max(); + + std::size_t index{invalid_index}; + std::uint32_t revision{}; + std::uint8_t sequence_number{}; + MmsStaticUrcbReportReason reason{MmsStaticUrcbReportReason::none}; +}; + +struct MmsStaticUrcbEncodeResult final { + MmsStaticUrcbStatus status{MmsStaticUrcbStatus::invalid_runtime}; + std::size_t bytes_written{}; + std::size_t required_bytes{}; + std::size_t member_count{}; + + [[nodiscard]] constexpr bool success() const noexcept { + return status == MmsStaticUrcbStatus::ok; + } +}; + +class MmsStaticUrcbRuntime final { +public: + static constexpr std::size_t maximum_control_blocks = 16U; + static constexpr std::uint32_t minimum_integrity_period_ms = 100U; + + MmsStaticUrcbRuntime( + std::span definitions, + std::span states, + const MmsStaticObjectTable& objects, + const MmsStaticDataSetTable& data_sets) noexcept + : definitions_{definitions}, + states_{states}, + objects_{&objects}, + data_sets_{&data_sets} {} + + [[nodiscard]] bool initialize() noexcept; + [[nodiscard]] constexpr bool valid() const noexcept { return initialized_; } + [[nodiscard]] constexpr std::size_t size() const noexcept { + return initialized_ ? definitions_.size() : 0U; + } + + [[nodiscard]] const MmsStaticUrcbDefinition* definition( + std::size_t index) const noexcept; + [[nodiscard]] const MmsStaticUrcbState* state(std::size_t index) const noexcept; + [[nodiscard]] MmsStaticUrcbState* state(std::size_t index) noexcept; + + [[nodiscard]] bool find_index( + const MmsObjectNameView& name, + std::size_t& index) const noexcept; + + [[nodiscard]] MmsStaticUrcbStatus set_enabled( + std::size_t index, + bool enabled, + std::uint64_t now_ms) noexcept; + [[nodiscard]] MmsStaticUrcbStatus set_reserved( + std::size_t index, + bool reserved) noexcept; + [[nodiscard]] MmsStaticUrcbStatus set_report_id( + std::size_t index, + std::string_view report_id) noexcept; + [[nodiscard]] MmsStaticUrcbStatus set_data_set( + std::size_t index, + std::string_view domain, + std::string_view item) noexcept; + [[nodiscard]] MmsStaticUrcbStatus set_optional_fields( + std::size_t index, + std::span optional_fields) noexcept; + [[nodiscard]] MmsStaticUrcbStatus set_buffer_time_ms( + std::size_t index, + std::uint32_t buffer_time_ms) noexcept; + [[nodiscard]] MmsStaticUrcbStatus set_trigger_options( + std::size_t index, + std::uint8_t trigger_options) noexcept; + [[nodiscard]] MmsStaticUrcbStatus set_integrity_period_ms( + std::size_t index, + std::uint32_t integrity_period_ms) noexcept; + [[nodiscard]] MmsStaticUrcbStatus request_general_interrogation( + std::size_t index) noexcept; + + [[nodiscard]] bool next_due( + std::uint64_t now_ms, + MmsStaticUrcbEmissionPlan& plan) const noexcept; + + [[nodiscard]] MmsStaticUrcbEncodeResult encode( + const MmsStaticUrcbEmissionPlan& plan, + std::span report_time, + std::span destination, + std::span workspace) const noexcept; + + [[nodiscard]] MmsStaticUrcbStatus commit( + const MmsStaticUrcbEmissionPlan& plan, + std::uint64_t now_ms) noexcept; + +private: + std::span definitions_{}; + std::span states_{}; + const MmsStaticObjectTable* objects_{}; + const MmsStaticDataSetTable* data_sets_{}; + bool initialized_{}; +}; + +} // namespace ar::iec61850::mms diff --git a/include/ariec61850/mms/utc_time.hpp b/include/ariec61850/mms/utc_time.hpp index 6b62af416..78d00ca1e 100644 --- a/include/ariec61850/mms/utc_time.hpp +++ b/include/ariec61850/mms/utc_time.hpp @@ -13,14 +13,24 @@ struct Iec61850UtcTime final { std::chrono::system_clock::time_point value{}; std::uint8_t quality{}; + // No-throw decode primitive for constrained/embedded profiles. + [[nodiscard]] static bool try_from_bytes( + std::span bytes, + Iec61850UtcTime& result) noexcept; + +#if !defined(ARIEC61850_NO_EXCEPTIONS) + // Host convenience wrapper retained when C++ exceptions are available. [[nodiscard]] static Iec61850UtcTime from_bytes(std::span bytes); +#endif // Allocation-free wire path for embedded publishers. Returns false when // the timestamp cannot be represented by IEC 61850's 32-bit seconds field. [[nodiscard]] bool try_write_bytes(std::span destination) const noexcept; +#if !defined(ARIEC61850_NO_EXCEPTIONS) // Host convenience wrapper retained for source compatibility. [[nodiscard]] std::vector to_bytes() const; +#endif friend bool operator==(const Iec61850UtcTime&, const Iec61850UtcTime&) = default; }; diff --git a/include/ariec61850/osi/cotp_span.hpp b/include/ariec61850/osi/cotp_span.hpp new file mode 100644 index 000000000..99a068250 --- /dev/null +++ b/include/ariec61850/osi/cotp_span.hpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include + +namespace ar::iec61850::osi { + +enum class CotpWireKind : std::uint8_t { + unknown, + connection_request, + connection_confirm, + data, + disconnect_request, + error, +}; + +struct CotpParameterView final { + std::uint8_t code{}; + std::span value{}; +}; + +struct CotpTpduView final { + CotpWireKind kind{CotpWireKind::unknown}; + std::uint8_t code{}; + std::uint8_t length_indicator{}; + std::uint16_t destination_reference{}; + std::uint16_t source_reference{}; + std::uint8_t class_or_reason{}; + bool end_of_transmission{}; + std::uint8_t tpdu_number{}; + std::span parameter_bytes{}; + std::span user_data{}; + + [[nodiscard]] bool try_parameter( + std::uint8_t parameter_code, + std::span& value) const noexcept; +}; + +class CotpSpanCodec final { +public: + static constexpr std::uint8_t connection_request_code = 0xE0U; + static constexpr std::uint8_t connection_confirm_code = 0xD0U; + static constexpr std::uint8_t disconnect_request_code = 0x80U; + static constexpr std::uint8_t data_code = 0xF0U; + static constexpr std::uint8_t error_code = 0x70U; + + static constexpr std::uint8_t tpdu_size_parameter = 0xC0U; + static constexpr std::uint8_t source_tsap_parameter = 0xC1U; + static constexpr std::uint8_t destination_tsap_parameter = 0xC2U; + + [[nodiscard]] static CotpWireKind kind_from_code(std::uint8_t code) noexcept; + + // Zero-copy decode. parameter_bytes and user_data point into the supplied + // TPDU storage, which must outlive the returned view. + [[nodiscard]] static bool try_decode_view( + std::span bytes, + CotpTpduView& tpdu) noexcept; + + [[nodiscard]] static bool try_tpdu_size_bytes( + std::uint8_t tpdu_size_code, + std::size_t& bytes) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_data_into( + std::span user_data, + std::span destination, + bool end_of_transmission = true, + std::uint8_t tpdu_number = 0U) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_connection_request_into( + std::uint16_t source_reference, + std::span parameters, + std::span destination, + std::uint16_t destination_reference = 0U, + std::uint8_t transport_class = 0U) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_connection_confirm_into( + std::uint16_t destination_reference, + std::uint16_t source_reference, + std::span destination, + std::uint8_t maximum_tpdu_size_code = 0x0AU) noexcept; + + // Build a server-side CC directly from a zero-copy CR view. The TPDU-size + // offer is negotiated and source/destination TSAP parameters are mirrored + // using the same wire behavior as the host codec, without heap storage. + [[nodiscard]] static wire::EncodeResult encode_connection_confirm_from_request_into( + const CotpTpduView& connection_request, + std::uint16_t source_reference, + std::span destination, + std::uint8_t maximum_tpdu_size_code = 0x0AU) noexcept; +}; + +} // namespace ar::iec61850::osi diff --git a/include/ariec61850/osi/presentation_span.hpp b/include/ariec61850/osi/presentation_span.hpp new file mode 100644 index 000000000..7afb688e1 --- /dev/null +++ b/include/ariec61850/osi/presentation_span.hpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::osi { + +struct PresentationPdvView final { + std::uint32_t context_id{}; + std::span single_asn1_type{}; +}; + +struct PresentationCpView final { + std::uint32_t mode_selector{1U}; + std::span calling_selector{}; + std::span called_selector{}; + std::span context_definition_list{}; + std::size_t context_count{}; + PresentationPdvView user_data{}; + + [[nodiscard]] bool try_context_id_for_abstract_syntax( + std::span object_identifier_value, + std::uint32_t& context_id) const noexcept; +}; + +class PresentationSpanCodec final { +public: + static constexpr std::size_t maximum_ppdu_bytes = 1U * 1024U * 1024U; + static constexpr std::size_t maximum_contexts = 64U; + static constexpr std::size_t maximum_oid_bytes = 64U; + static constexpr std::size_t maximum_selector_bytes = 64U; + + [[nodiscard]] static std::span + acse_abstract_syntax_name() noexcept; + [[nodiscard]] static std::span + mms_abstract_syntax_name() noexcept; + [[nodiscard]] static std::span + ber_transfer_syntax_name() noexcept; + + [[nodiscard]] static std::optional fully_encoded_data_size( + std::uint32_t context_id, + std::size_t single_asn1_type_bytes) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_fully_encoded_data_into( + std::uint32_t context_id, + std::span single_asn1_type, + std::span destination) noexcept; + + [[nodiscard]] static bool try_decode_fully_encoded_data_view( + std::span bytes, + PresentationPdvView& pdv) noexcept; + + [[nodiscard]] static bool try_decode_cp_view( + std::span bytes, + PresentationCpView& cp) noexcept; + + [[nodiscard]] static std::optional cpa_accepting_size( + const PresentationCpView& request, + std::size_t acse_aare_bytes) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_cpa_accepting_into( + const PresentationCpView& request, + std::span acse_aare, + std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_p_data_into( + std::span abstract_syntax_payload, + std::span destination, + std::uint32_t presentation_context_id = 3U, + bool include_give_tokens_prefix = true) noexcept; + + [[nodiscard]] static bool try_decode_p_data_view( + std::span bytes, + PresentationPdvView& pdv) noexcept; +}; + +} // namespace ar::iec61850::osi diff --git a/include/ariec61850/osi/session_span.hpp b/include/ariec61850/osi/session_span.hpp new file mode 100644 index 000000000..cc0667c90 --- /dev/null +++ b/include/ariec61850/osi/session_span.hpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include + +namespace ar::iec61850::osi { + +enum class SessionWireKind : std::uint8_t { + unknown, + connect, + accept, + reject, + refuse, + abort, + data_transfer, +}; + +struct SessionParameterView final { + std::uint8_t code{}; + std::span value{}; +}; + +struct SessionSpduView final { + SessionWireKind kind{SessionWireKind::unknown}; + std::uint8_t code{}; + std::uint8_t length_indicator{}; + std::span parameter_bytes{}; + std::span user_data{}; + + [[nodiscard]] bool try_parameter( + std::uint8_t parameter_code, + std::span& value) const noexcept; +}; + +struct SessionDataTransferView final { + bool has_give_tokens_prefix{}; + std::span presentation_payload{}; +}; + +class SessionSpanCodec final { +public: + static constexpr std::uint8_t connect_code = 0x0DU; + static constexpr std::uint8_t accept_code = 0x0EU; + static constexpr std::uint8_t reject_code = 0x0AU; + static constexpr std::uint8_t refuse_code = 0x0CU; + static constexpr std::uint8_t abort_code = 0x19U; + static constexpr std::uint8_t data_transfer_code = 0x01U; + static constexpr std::uint8_t user_data_parameter = 0xC1U; + static constexpr std::size_t maximum_spdu_bytes = 257U; + static constexpr std::size_t maximum_parameter_count = 64U; + + [[nodiscard]] static SessionWireKind kind_from_code(std::uint8_t code) noexcept; + [[nodiscard]] static std::span default_parameters() noexcept; + + [[nodiscard]] static bool try_decode_prefix_view( + std::span bytes, + SessionSpduView& spdu, + std::size_t& consumed) noexcept; + + [[nodiscard]] static bool try_decode_view( + std::span bytes, + SessionSpduView& spdu) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_into( + std::uint8_t code, + std::span parameters, + std::span user_data, + std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_connect_into( + std::span presentation_payload, + std::span destination, + std::span parameters = {}) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_accept_into( + std::span presentation_payload, + std::span destination, + std::span parameters = {}) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_accept_mirroring_into( + const SessionSpduView& connect, + std::span presentation_payload, + std::span destination) noexcept; + + [[nodiscard]] static wire::EncodeResult encode_data_transfer_into( + std::span presentation_payload, + std::span destination, + bool include_give_tokens_prefix = true) noexcept; + + [[nodiscard]] static bool try_decode_data_transfer_view( + std::span bytes, + SessionDataTransferView& transfer) noexcept; +}; + +} // namespace ar::iec61850::osi diff --git a/include/ariec61850/osi/tpkt_span.hpp b/include/ariec61850/osi/tpkt_span.hpp new file mode 100644 index 000000000..07327ebe8 --- /dev/null +++ b/include/ariec61850/osi/tpkt_span.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include "ariec61850/wire/encode_result.hpp" + +#include +#include +#include + +namespace ar::iec61850::osi { + +enum class TpktPeekStatus : std::uint8_t { + need_more, + ready, + invalid, +}; + +struct TpktPeekResult final { + TpktPeekStatus status{TpktPeekStatus::need_more}; + std::size_t frame_bytes{}; + + [[nodiscard]] constexpr bool ready() const noexcept { + return status == TpktPeekStatus::ready; + } +}; + +struct TpktFrameView final { + std::uint8_t version{}; + std::uint16_t declared_length{}; + std::span payload{}; +}; + +class TpktSpanCodec final { +public: + static constexpr std::uint8_t supported_version = 0x03U; + static constexpr std::size_t header_length = 4U; + static constexpr std::size_t maximum_frame_bytes = 65'535U; + static constexpr std::size_t maximum_payload_bytes = + maximum_frame_bytes - header_length; + + [[nodiscard]] static wire::EncodeResult encode_into( + std::span payload, + std::span destination) noexcept; + + // Inspect the first frame in a TCP receive window without copying it. + // `ready` may be returned even when bytes contains coalesced frames; + // frame_bytes identifies the exact prefix belonging to the first TPKT. + [[nodiscard]] static TpktPeekResult peek_frame( + std::span bytes) noexcept; + + // Decode one exact TPKT frame into non-owning spans. The supplied storage + // must outlive the returned view. + [[nodiscard]] static bool try_decode_view( + std::span bytes, + TpktFrameView& frame) noexcept; +}; + +} // namespace ar::iec61850::osi diff --git a/scripts/build_mms_direct_control_server.ps1 b/scripts/build_mms_direct_control_server.ps1 new file mode 100644 index 000000000..f8f41703a --- /dev/null +++ b/scripts/build_mms_direct_control_server.ps1 @@ -0,0 +1,112 @@ +param( + [string]$BuildDir = "build-mms-direct-control" +) + +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $PSScriptRoot +$Source = Join-Path $Root "tools\mms_direct_control_server" +$Build = Join-Path $Root $BuildDir + +function Invoke-Checked { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Arguments + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$FilePath failed with exit code $LASTEXITCODE." + } +} + +function Repair-MissingTrackedFiles { + $insideWorkTree = (& git -C $Root rev-parse --is-inside-work-tree 2>$null) + if ($LASTEXITCODE -ne 0 -or $insideWorkTree -ne "true") { + return + } + + $tracked = @(& git -C $Root ls-files) + if ($LASTEXITCODE -ne 0) { + throw "Unable to enumerate tracked files before the MMS server build." + } + + $missing = @( + $tracked | Where-Object { + $_ -and -not (Test-Path -LiteralPath (Join-Path $Root $_)) + } + ) + + if ($missing.Count -eq 0) { + return + } + + Write-Host "[repair] Restoring $($missing.Count) tracked file(s) missing from the working tree..." + foreach ($relativePath in $missing) { + Write-Host "[repair] $relativePath" + } + + & git -C $Root restore --worktree --source=HEAD -- @missing + if ($LASTEXITCODE -ne 0) { + throw "Unable to restore tracked files missing from the working tree. Run 'git status --short' and inspect the checkout before retrying." + } +} + +Repair-MissingTrackedFiles + +$requiredPaths = @( + "CMakeLists.txt", + "src\mms\association_runtime_control_wait.cpp", + "src\mms\static_report_session.cpp", + "src\control\guarded_control.cpp", + "src\control\mms_control_structure.cpp", + "src\control\command_termination.cpp", + "src\control\control_discovery.cpp", + "src\control\control_session.cpp", + "src\control\mms_association_control_transport.cpp", + "src\mms\static_direct_control.cpp", + "src\mms\static_information_report.cpp", + "src\mms\static_report_connection.cpp", + "src\mms\static_urcb_objects.cpp", + "src\mms\static_urcb_runtime.cpp", + "tools\mms_direct_control_server.cpp", + "tools\mms_urcb_server.cpp", + "tools\mms_direct_control_server\CMakeLists.txt" +) + +$stillMissing = @( + $requiredPaths | Where-Object { + -not (Test-Path -LiteralPath (Join-Path $Root $_)) + } +) +if ($stillMissing.Count -ne 0) { + $formatted = ($stillMissing | ForEach-Object { " - $_" }) -join [Environment]::NewLine + throw "MMS server build prerequisites are still missing after checkout repair:$([Environment]::NewLine)$formatted$([Environment]::NewLine)Verify that HEAD is agent/iedsim-wire-parity-current and run 'git status --short'." +} + +Write-Host "[OK] MMS R1-R2 current-lineage source preflight passed." + +Invoke-Checked cmake -S $Source -B $Build -DCMAKE_BUILD_TYPE=Release +Invoke-Checked cmake --build $Build --config Release --parallel +Invoke-Checked ctest --test-dir $Build -C Release --output-on-failure + +function Find-BuiltExe([string]$Name) { + $candidates = @( + (Join-Path $Build "Release\$Name.exe"), + (Join-Path $Build "$Name.exe") + ) + return $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1 +} + +$controlExe = Find-BuiltExe "ariec61850_mms_direct_control_server" +$urcbExe = Find-BuiltExe "ariec61850_mms_urcb_server" +if (-not $controlExe) { + throw "Build succeeded but ariec61850_mms_direct_control_server.exe was not found." +} +if (-not $urcbExe) { + throw "Build succeeded but ariec61850_mms_urcb_server.exe was not found." +} + +Write-Host "[OK] MMS Direct Control server: $controlExe" +Write-Host "[OK] MMS URCB Reporting server: $urcbExe" diff --git a/scripts/test-mms-server-live-discovery.py b/scripts/test-mms-server-live-discovery.py new file mode 100644 index 000000000..196b23ac1 --- /dev/null +++ b/scripts/test-mms-server-live-discovery.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +"""Loopback contract between runnable MMS servers and live_discover.""" + +from __future__ import annotations + +import argparse +import json +import os +import socket +import subprocess +import sys +import time +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Profile: + name: str + server: str + server_args: tuple[str, ...] + logical_nodes: int + data_objects: int + data_attributes: int + data_sets: int + report_controls: int + type_probes: int + exact_types: int + + +def free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: + listener.bind(("127.0.0.1", 0)) + return int(listener.getsockname()[1]) + + +def creation_flags() -> int: + return subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 + + +def run_profile(client: str, profile: Profile) -> None: + port = free_port() + command = [ + profile.server, + "--bind", + "127.0.0.1", + "--port", + str(port), + *profile.server_args, + "--once", + ] + server = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + creationflags=creation_flags(), + ) + client_result: subprocess.CompletedProcess[str] | None = None + try: + time.sleep(0.5) + client_result = subprocess.run( + [ + client, + "127.0.0.1", + str(port), + "--model-json", + "--timeout-ms", + "3000", + ], + capture_output=True, + text=True, + timeout=15, + check=False, + ) + server_stdout, server_stderr = server.communicate(timeout=8) + except BaseException: + server.kill() + server_stdout, server_stderr = server.communicate() + raise + + if client_result.returncode != 0 or server.returncode != 0: + raise RuntimeError( + f"{profile.name} loopback failed: client={client_result.returncode}, " + f"server={server.returncode}\n" + f"client stdout:\n{client_result.stdout}\n" + f"client stderr:\n{client_result.stderr}\n" + f"server stdout:\n{server_stdout}\n" + f"server stderr:\n{server_stderr}" + ) + + document = json.loads(client_result.stdout) + coverage = document["coverage"] + expected = { + "logicalDeviceCount": 1, + "logicalNodeCount": profile.logical_nodes, + "dataObjectCount": profile.data_objects, + "dataAttributeCount": profile.data_attributes, + "dataSetCount": profile.data_sets, + "reportControlCount": profile.report_controls, + "variableTypeReadAttemptCount": profile.type_probes, + "variableTypeReadSuccessCount": profile.type_probes, + "variableTypeReadFailureCount": 0, + "exactMmsTypeCount": profile.exact_types, + } + mismatches = { + key: (coverage.get(key), value) + for key, value in expected.items() + if coverage.get(key) != value + } + warnings = document.get("warnings", []) + if mismatches or warnings: + raise RuntimeError( + f"{profile.name} discovery mismatch: {mismatches}; warnings={warnings}" + ) + if "CLIENT_ACCEPTED" not in server_stdout or "CLIENT_CLOSED" not in server_stdout: + raise RuntimeError( + f"{profile.name} server lifecycle evidence missing:\n{server_stdout}" + ) + + print( + f"{profile.name}: LD=1 LN={profile.logical_nodes} " + f"DO={profile.data_objects} DA={profile.data_attributes} " + f"DataSet={profile.data_sets} RCB={profile.report_controls} " + f"exactTypes={profile.exact_types}" + ) + + +def run_direct_control(inventory: str, client: str, server_executable: str) -> None: + port = free_port() + server = subprocess.Popen( + [server_executable, "--bind", "127.0.0.1", "--port", str(port)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + creationflags=creation_flags(), + ) + try: + time.sleep(0.5) + inventory_result = subprocess.run( + [inventory, "127.0.0.1", str(port), "--timeout-ms", "3000"], + capture_output=True, + text=True, + timeout=15, + check=False, + ) + operate_result = subprocess.run( + [ + client, + "127.0.0.1", + str(port), + "--object", + "ESP32S3IOLD0/GGIO1.SPCSO1", + "--action", + "operate", + "--value", + "true", + "--synchro-check", + "off", + "--interlock-check", + "off", + "--arm", + "IEC61850-LAB-CONTROL", + "--timeout-ms", + "3000", + ], + capture_output=True, + text=True, + timeout=15, + check=False, + ) + verify_result = subprocess.run( + [ + client, + "127.0.0.1", + str(port), + "--object", + "ESP32S3IOLD0/GGIO1.SPCSO1", + "--timeout-ms", + "3000", + ], + capture_output=True, + text=True, + timeout=15, + check=False, + ) + finally: + server.terminate() + try: + server_stdout, server_stderr = server.communicate(timeout=8) + except subprocess.TimeoutExpired: + server.kill() + server_stdout, server_stderr = server.communicate() + + failures = [] + if inventory_result.returncode != 0 or "candidates=8 ready=8" not in inventory_result.stdout: + failures.append( + f"inventory exit={inventory_result.returncode}\n" + f"stdout:\n{inventory_result.stdout}\nstderr:\n{inventory_result.stderr}" + ) + if ( + operate_result.returncode != 0 + or "completion=accepted accepted=true" not in operate_result.stdout + or "STATUS_AFTER true" not in operate_result.stdout + or "NO_RETRY_EVIDENCE controlWrites=1" not in operate_result.stdout + ): + failures.append( + f"operate exit={operate_result.returncode}\n" + f"stdout:\n{operate_result.stdout}\nstderr:\n{operate_result.stderr}" + ) + if verify_result.returncode != 0 or "STATUS_BEFORE true" not in verify_result.stdout: + failures.append( + f"verify exit={verify_result.returncode}\n" + f"stdout:\n{verify_result.stdout}\nstderr:\n{verify_result.stderr}" + ) + if failures: + raise RuntimeError( + "direct control loopback failed:\n" + + "\n".join(failures) + + f"\nserver stdout:\n{server_stdout}\nserver stderr:\n{server_stderr}" + ) + print("direct-control: candidates=8 ready=8 operate=accepted status=true writes=1") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--client", required=True) + parser.add_argument("--control-inventory", required=True) + parser.add_argument("--control-client", required=True) + parser.add_argument("--direct-server", required=True) + parser.add_argument("--urcb-server", required=True) + args = parser.parse_args() + + profiles = ( + Profile( + "direct", + args.direct_server, + ("--di-mask", "0xA5", "--do-mask", "0x3C"), + 3, + 18, + 34, + 1, + 0, + 3, + 34, + ), + Profile( + "urcb", + args.urcb_server, + ("--mask", "0x3C"), + 2, + 9, + 19, + 1, + 1, + 2, + 19, + ), + ) + for profile in profiles: + run_profile(args.client, profile) + run_direct_control( + args.control_inventory, args.control_client, args.direct_server + ) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: # noqa: BLE001 - evidence must include all failures. + print(f"MMS server loopback failed: {error}", file=sys.stderr) + raise SystemExit(1) diff --git a/src/acse/association_compat_span.cpp b/src/acse/association_compat_span.cpp new file mode 100644 index 000000000..9cbbec9a7 --- /dev/null +++ b/src/acse/association_compat_span.cpp @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/acse/association_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" + +#include +#include +#include + +namespace ar::iec61850::acse { +namespace { + +[[nodiscard]] bool valid_oid(const std::span value) noexcept { + return !value.empty() && value.size() <= AcseSpanCodec::maximum_oid_bytes; +} + +[[nodiscard]] bool read_positive_integer( + const asn1::BerTlvView& tlv, + std::uint32_t& value) noexcept { + value = 0U; + const auto decoded = asn1::BerSpanReader::read_uint32(tlv); + if (!decoded || *decoded == 0U) { + return false; + } + value = *decoded; + return true; +} + +[[nodiscard]] bool count_contexts( + const std::span list, + std::size_t& count) noexcept { + count = 0U; + std::size_t offset = 0U; + while (offset < list.size()) { + if (count >= osi::PresentationSpanCodec::maximum_contexts) { + count = 0U; + return false; + } + asn1::BerTlvView definition; + if (!asn1::BerSpanReader::try_read_tlv(list, offset, definition) || + definition.tag_class != asn1::BerClass::universal || + definition.tag_number != 16 || !definition.constructed) { + count = 0U; + return false; + } + ++count; + } + return count != 0U; +} + +[[nodiscard]] bool decode_pdv( + const asn1::BerTlvView& outer, + osi::PresentationPdvView& pdv) noexcept { + pdv = {}; + if (outer.tag_class != asn1::BerClass::application || + outer.tag_number != 1 || !outer.constructed) { + return false; + } + + asn1::BerTlvView sequence; + if (!asn1::BerSpanReader::try_read_exact(outer.value, sequence) || + sequence.tag_class != asn1::BerClass::universal || + sequence.tag_number != 16 || !sequence.constructed) { + return false; + } + + bool saw_context = false; + bool saw_transfer_syntax = false; + bool saw_payload = false; + std::size_t offset = 0U; + while (offset < sequence.value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(sequence.value, offset, field)) { + pdv = {}; + return false; + } + if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 2 && !field.constructed) { + if (saw_context || !read_positive_integer(field, pdv.context_id)) { + pdv = {}; + return false; + } + saw_context = true; + } else if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 6 && !field.constructed) { + if (saw_transfer_syntax || !valid_oid(field.value)) { + pdv = {}; + return false; + } + saw_transfer_syntax = true; + } else if (field.tag_class == asn1::BerClass::context_specific && + field.tag_number == 0 && field.constructed) { + if (saw_payload || field.value.empty()) { + pdv = {}; + return false; + } + pdv.single_asn1_type = field.value; + saw_payload = true; + } else { + pdv = {}; + return false; + } + } + return saw_context && saw_payload; +} + +[[nodiscard]] bool decode_cp( + const std::span bytes, + osi::PresentationCpView& cp) noexcept { + cp = {}; + cp.mode_selector = 1U; + if (bytes.empty() || bytes.size() > osi::PresentationSpanCodec::maximum_ppdu_bytes) { + return false; + } + + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(bytes, outer) || + outer.tag_class != asn1::BerClass::universal || + outer.tag_number != 17 || !outer.constructed) { + return false; + } + + bool saw_mode = false; + bool saw_normal = false; + std::size_t offset = 0U; + while (offset < outer.value.size()) { + asn1::BerTlvView item; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, item)) { + cp = {}; + return false; + } + + if (item.tag_class == asn1::BerClass::context_specific && + item.tag_number == 0 && item.constructed) { + if (saw_mode) { + cp = {}; + return false; + } + asn1::BerTlvView mode; + if (!asn1::BerSpanReader::try_read_exact(item.value, mode) || + mode.tag_class != asn1::BerClass::context_specific || + mode.tag_number != 0 || mode.constructed || + !read_positive_integer(mode, cp.mode_selector)) { + cp = {}; + return false; + } + saw_mode = true; + continue; + } + + // The proven ARIEC61850 simulator ignored harmless optional CP fields + // outside the normal-mode parameter instead of rejecting association. + if (item.tag_class != asn1::BerClass::context_specific || + item.tag_number != 2 || !item.constructed) { + continue; + } + if (saw_normal) { + cp = {}; + return false; + } + + bool saw_calling = false; + bool saw_called = false; + bool saw_contexts = false; + bool saw_user_data = false; + std::size_t normal_offset = 0U; + while (normal_offset < item.value.size()) { + asn1::BerTlvView normal; + if (!asn1::BerSpanReader::try_read_tlv(item.value, normal_offset, normal)) { + cp = {}; + return false; + } + if (normal.tag_class == asn1::BerClass::context_specific && + normal.tag_number == 1 && !normal.constructed) { + if (saw_calling || normal.value.size() > + osi::PresentationSpanCodec::maximum_selector_bytes) { + cp = {}; + return false; + } + cp.calling_selector = normal.value; + saw_calling = true; + } else if (normal.tag_class == asn1::BerClass::context_specific && + normal.tag_number == 2 && !normal.constructed) { + if (saw_called || normal.value.size() > + osi::PresentationSpanCodec::maximum_selector_bytes) { + cp = {}; + return false; + } + cp.called_selector = normal.value; + saw_called = true; + } else if (normal.tag_class == asn1::BerClass::context_specific && + normal.tag_number == 4 && normal.constructed) { + if (saw_contexts || !count_contexts( + normal.value, cp.context_count)) { + cp = {}; + return false; + } + cp.context_definition_list = normal.value; + saw_contexts = true; + } else if (normal.tag_class == asn1::BerClass::application && + normal.tag_number == 1 && normal.constructed) { + if (saw_user_data || !decode_pdv(normal, cp.user_data)) { + cp = {}; + return false; + } + saw_user_data = true; + } else { + // Optional Presentation fields are intentionally ignored here. + continue; + } + } + if (!saw_contexts || !saw_user_data) { + cp = {}; + return false; + } + saw_normal = true; + } + return saw_normal && cp.mode_selector != 0U; +} + +[[nodiscard]] bool decode_external( + const asn1::BerTlvView& user_information, + AcseExternalView& external) noexcept { + external = {}; + if (!user_information.constructed) { + return false; + } + + bool saw_external = false; + std::size_t ui_offset = 0U; + while (ui_offset < user_information.value.size()) { + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_tlv( + user_information.value, ui_offset, outer)) { + return false; + } + if (outer.tag_class != asn1::BerClass::universal || + outer.tag_number != 8 || !outer.constructed) { + continue; + } + if (saw_external) { + return false; + } + saw_external = true; + + std::size_t offset = 0U; + while (offset < outer.value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, field)) { + external = {}; + return false; + } + if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 6 && !field.constructed && + external.direct_reference.empty()) { + if (!valid_oid(field.value)) { + external = {}; + return false; + } + external.direct_reference = field.value; + } else if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 2 && !field.constructed && + external.indirect_reference == 0U) { + const auto value = asn1::BerSpanReader::read_uint32(field); + if (!value) { + external = {}; + return false; + } + external.indirect_reference = *value; + } else if (field.tag_class == asn1::BerClass::context_specific && + field.tag_number == 0 && field.constructed) { + if (!external.single_asn1_type.empty() || field.value.empty()) { + external = {}; + return false; + } + external.single_asn1_type = field.value; + } + // Descriptor and other optional EXTERNAL fields are ignored. + } + } + return saw_external && !external.single_asn1_type.empty(); +} + +[[nodiscard]] bool decode_aarq( + const std::span bytes, + AcseAarqView& aarq) noexcept { + aarq = {}; + if (bytes.empty() || bytes.size() > AcseSpanCodec::maximum_acse_bytes) { + return false; + } + + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(bytes, outer) || + outer.tag_class != asn1::BerClass::application || + outer.tag_number != 0 || !outer.constructed) { + return false; + } + + bool saw_user_information = false; + std::size_t offset = 0U; + while (offset < outer.value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, field)) { + aarq = {}; + return false; + } + if (field.tag_class == asn1::BerClass::context_specific && + field.tag_number == 30 && field.constructed) { + if (saw_user_information || !decode_external( + field, aarq.user_information)) { + aarq = {}; + return false; + } + saw_user_information = true; + } + } + if (!saw_user_information) { + aarq = {}; + return false; + } + + asn1::BerTlvView initiate; + return asn1::BerSpanReader::try_read_exact( + aarq.user_information.single_asn1_type, initiate) && + initiate.tag_class == asn1::BerClass::context_specific && + initiate.tag_number == 8 && initiate.constructed; +} + +[[nodiscard]] bool valid_request(const AssociationRequestView& request) noexcept { + return request.session.kind == osi::SessionWireKind::connect && + request.presentation.mode_selector != 0U && + request.presentation.context_count != 0U && + request.acse_presentation_context_id != 0U && + request.mms_presentation_context_id != 0U && + request.acse_presentation_context_id == request.presentation.user_data.context_id; +} + +} // namespace + +bool AcseSpanCodec::try_decode_association_request_compat_view( + const std::span bytes, + AssociationRequestView& request) noexcept { + if (try_decode_association_request_view(bytes, request)) { + return true; + } + + request = {}; + if (!osi::SessionSpanCodec::try_decode_view(bytes, request.session) || + request.session.kind != osi::SessionWireKind::connect || + !decode_cp(request.session.user_data, request.presentation) || + !decode_aarq( + request.presentation.user_data.single_asn1_type, request.aarq)) { + request = {}; + return false; + } + + if (!request.presentation.try_context_id_for_abstract_syntax( + osi::PresentationSpanCodec::acse_abstract_syntax_name(), + request.acse_presentation_context_id) || + !request.presentation.try_context_id_for_abstract_syntax( + osi::PresentationSpanCodec::mms_abstract_syntax_name(), + request.mms_presentation_context_id) || + !valid_request(request)) { + request = {}; + return false; + } + return true; +} + +} // namespace ar::iec61850::acse diff --git a/src/acse/association_span.cpp b/src/acse/association_span.cpp new file mode 100644 index 000000000..4caa894db --- /dev/null +++ b/src/acse/association_span.cpp @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/acse/association_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::acse { +namespace { + +constexpr std::array kMmsApplicationContext{ + 0x28U, 0xCAU, 0x22U, 0x02U, 0x03U}; + +constexpr std::array kDefaultMmsInitiateRequest{ + 0xA8U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0xEEU, 0x1CU, 0x00U, 0x00U, 0x04U, + 0x08U, 0x00U, 0x00U, 0x79U, 0xEFU, 0x18U}; + +// servicesSupportedCalled contains only the confirmed services implemented by +// MmsStaticApplicationDispatcher: GetNameList(1), Read(4), Write(5), +// GetVariableAccessAttributes(6), and GetNamedVariableListAttributes(12). +constexpr std::array kDefaultMmsInitiateResponse{ + 0xA9U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +// Byte-identical to the deterministic static AARE profile used by the bounded +// server. The embedded MMS Initiate response carries the same narrow service +// bitmap as kDefaultMmsInitiateResponse above. +constexpr std::array kDefaultAcceptAare{ + 0x61U, 0x4AU, + 0xA1U, 0x07U, 0x06U, 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, 0x03U, + 0xA2U, 0x03U, 0x02U, 0x01U, 0x00U, + 0xA3U, 0x05U, 0xA1U, 0x03U, 0x02U, 0x01U, 0x00U, + 0xBEU, 0x33U, + 0x28U, 0x31U, + 0x06U, 0x02U, 0x51U, 0x01U, + 0x02U, 0x01U, 0x03U, + 0xA0U, 0x28U, + 0xA9U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +[[nodiscard]] std::optional accept_aare_size( + const std::size_t mms_initiate_response_bytes) noexcept { + const auto application_context_oid = asn1::BerSpanWriter::tlv_size( + 6, kMmsApplicationContext.size()); + const auto application_context = application_context_oid + ? asn1::BerSpanWriter::tlv_size(1, *application_context_oid) + : std::nullopt; + const auto result_integer = asn1::BerSpanWriter::tlv_size(2, 1U); + const auto result = result_integer + ? asn1::BerSpanWriter::tlv_size(2, *result_integer) + : std::nullopt; + const auto diagnostic_integer = asn1::BerSpanWriter::tlv_size(2, 1U); + const auto diagnostic_choice = diagnostic_integer + ? asn1::BerSpanWriter::tlv_size(1, *diagnostic_integer) + : std::nullopt; + const auto diagnostic = diagnostic_choice + ? asn1::BerSpanWriter::tlv_size(3, *diagnostic_choice) + : std::nullopt; + const auto direct_reference = asn1::BerSpanWriter::tlv_size(6, 2U); + const auto indirect_reference = asn1::BerSpanWriter::tlv_size(2, 1U); + const auto single_asn1_type = asn1::BerSpanWriter::tlv_size( + 0, mms_initiate_response_bytes); + if (!application_context || !result || !diagnostic || + !direct_reference || !indirect_reference || !single_asn1_type) { + return std::nullopt; + } + const auto external_content = + *direct_reference + *indirect_reference + *single_asn1_type; + const auto external = asn1::BerSpanWriter::tlv_size(8, external_content); + if (!external) { + return std::nullopt; + } + const auto user_information = asn1::BerSpanWriter::tlv_size(30, *external); + if (!user_information) { + return std::nullopt; + } + const auto aare_content = + *application_context + *result + *diagnostic + *user_information; + return asn1::BerSpanWriter::tlv_size(1, aare_content); +} + +[[nodiscard]] wire::EncodeResult encode_accept_aare_into( + const std::span mms_initiate_response, + const std::span destination) noexcept { + const auto required = accept_aare_size(mms_initiate_response.size()); + if (!required) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + const auto application_context_oid = asn1::BerSpanWriter::tlv_size( + 6, kMmsApplicationContext.size()).value(); + const auto result_integer = asn1::BerSpanWriter::tlv_size(2, 1U).value(); + const auto diagnostic_integer = asn1::BerSpanWriter::tlv_size(2, 1U).value(); + const auto diagnostic_choice = asn1::BerSpanWriter::tlv_size( + 1, diagnostic_integer).value(); + const auto direct_reference = asn1::BerSpanWriter::tlv_size(6, 2U).value(); + const auto indirect_reference = asn1::BerSpanWriter::tlv_size(2, 1U).value(); + const auto single_asn1_type = asn1::BerSpanWriter::tlv_size( + 0, mms_initiate_response.size()).value(); + const auto external_content = + direct_reference + indirect_reference + single_asn1_type; + const auto external = asn1::BerSpanWriter::tlv_size(8, external_content).value(); + const auto user_information = asn1::BerSpanWriter::tlv_size(30, external).value(); + const auto application_context = asn1::BerSpanWriter::tlv_size( + 1, application_context_oid).value(); + const auto result = asn1::BerSpanWriter::tlv_size(2, result_integer).value(); + const auto diagnostic = asn1::BerSpanWriter::tlv_size( + 3, diagnostic_choice).value(); + const auto aare_content = application_context + result + diagnostic + user_information; + + constexpr std::array direct_reference_oid{0x51U, 0x01U}; + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header(asn1::BerClass::application, true, 1, aare_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, application_context_oid) || + !writer.write_tlv( + asn1::BerClass::universal, false, 6, kMmsApplicationContext) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 2, result_integer) || + !writer.write_tlv_header(asn1::BerClass::universal, false, 2, 1U) || + !writer.write_byte(0U) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 3, diagnostic_choice) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, diagnostic_integer) || + !writer.write_tlv_header(asn1::BerClass::universal, false, 2, 1U) || + !writer.write_byte(0U) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 30, external) || + !writer.write_tlv_header( + asn1::BerClass::universal, true, 8, external_content) || + !writer.write_tlv( + asn1::BerClass::universal, false, 6, direct_reference_oid) || + !writer.write_tlv_header(asn1::BerClass::universal, false, 2, 1U) || + !writer.write_byte(3U) || + !writer.write_tlv( + asn1::BerClass::context_specific, + true, + 0, + mms_initiate_response) || + writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +[[nodiscard]] bool valid_oid( + const std::span value, + const bool allow_empty = false) noexcept { + return (allow_empty || !value.empty()) && value.size() <= AcseSpanCodec::maximum_oid_bytes; +} + +[[nodiscard]] bool decode_explicit_oid( + const asn1::BerTlvView& field, + std::span& oid, + const bool allow_empty = false) noexcept { + oid = {}; + if (!field.constructed) { + return false; + } + asn1::BerTlvView inner; + if (!asn1::BerSpanReader::try_read_exact(field.value, inner) || + inner.tag_class != asn1::BerClass::universal || + inner.tag_number != 6 || inner.constructed || + !valid_oid(inner.value, allow_empty)) { + return false; + } + oid = inner.value; + return true; +} + +[[nodiscard]] bool decode_explicit_integer( + const asn1::BerTlvView& field, + std::uint32_t& value) noexcept { + value = 0U; + if (!field.constructed) { + return false; + } + asn1::BerTlvView inner; + if (!asn1::BerSpanReader::try_read_exact(field.value, inner) || + inner.tag_class != asn1::BerClass::universal || + inner.tag_number != 2 || inner.constructed) { + return false; + } + const auto decoded = asn1::BerSpanReader::read_uint32(inner); + if (!decoded) { + return false; + } + value = *decoded; + return true; +} + +[[nodiscard]] bool decode_external( + const asn1::BerTlvView& user_information, + AcseExternalView& external) noexcept { + external = {}; + if (!user_information.constructed) { + return false; + } + + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(user_information.value, outer) || + outer.tag_class != asn1::BerClass::universal || + outer.tag_number != 8 || !outer.constructed) { + return false; + } + + bool saw_direct = false; + bool saw_indirect = false; + bool saw_encoding = false; + std::size_t offset = 0U; + while (offset < outer.value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, field)) { + external = {}; + return false; + } + + if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 6 && !field.constructed) { + if (saw_direct || !valid_oid(field.value)) { + external = {}; + return false; + } + external.direct_reference = field.value; + saw_direct = true; + } else if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 2 && !field.constructed) { + if (saw_indirect) { + external = {}; + return false; + } + const auto value = asn1::BerSpanReader::read_uint32(field); + if (!value) { + external = {}; + return false; + } + external.indirect_reference = *value; + saw_indirect = true; + } else if (field.tag_class == asn1::BerClass::context_specific && + field.tag_number == 0 && field.constructed) { + if (saw_encoding || field.value.size() > AcseSpanCodec::maximum_acse_bytes) { + external = {}; + return false; + } + external.single_asn1_type = field.value; + saw_encoding = true; + } else { + external = {}; + return false; + } + } + return saw_direct && saw_indirect && saw_encoding; +} + +[[nodiscard]] bool request_is_valid_for_response( + const AssociationRequestView& request) noexcept { + return request.session.kind == osi::SessionWireKind::connect && + request.presentation.mode_selector != 0U && + request.presentation.context_count != 0U && + request.acse_presentation_context_id != 0U && + request.mms_presentation_context_id != 0U && + request.acse_presentation_context_id == request.presentation.user_data.context_id; +} + +} // namespace + +std::span AcseSpanCodec::mms_application_context_name() noexcept { + return kMmsApplicationContext; +} + +std::span AcseSpanCodec::default_mms_initiate_request() noexcept { + return kDefaultMmsInitiateRequest; +} + +std::span AcseSpanCodec::default_mms_initiate_response() noexcept { + return kDefaultMmsInitiateResponse; +} + +bool AcseSpanCodec::try_decode_aarq_view( + const std::span bytes, + AcseAarqView& aarq) noexcept { + aarq = {}; + if (bytes.empty() || bytes.size() > maximum_acse_bytes) { + return false; + } + + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(bytes, outer) || + outer.tag_class != asn1::BerClass::application || + outer.tag_number != 0 || !outer.constructed) { + return false; + } + + bool saw_application = false; + bool saw_called_ap = false; + bool saw_called_ae = false; + bool saw_calling_ap = false; + bool saw_calling_ae = false; + bool saw_user_information = false; + std::size_t offset = 0U; + while (offset < outer.value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, field) || + field.tag_class != asn1::BerClass::context_specific) { + aarq = {}; + return false; + } + + switch (field.tag_number) { + case 1: + if (saw_application || + !decode_explicit_oid(field, aarq.application_context_name)) { + aarq = {}; + return false; + } + saw_application = true; + break; + case 2: + if (saw_called_ap || + !decode_explicit_oid(field, aarq.called_ap_title, true)) { + aarq = {}; + return false; + } + saw_called_ap = true; + break; + case 3: { + if (saw_called_ae) { + aarq = {}; + return false; + } + std::uint32_t value{}; + if (!decode_explicit_integer(field, value)) { + aarq = {}; + return false; + } + aarq.called_ae_qualifier = value; + saw_called_ae = true; + break; + } + case 6: + if (saw_calling_ap || + !decode_explicit_oid(field, aarq.calling_ap_title, true)) { + aarq = {}; + return false; + } + saw_calling_ap = true; + break; + case 7: { + if (saw_calling_ae) { + aarq = {}; + return false; + } + std::uint32_t value{}; + if (!decode_explicit_integer(field, value)) { + aarq = {}; + return false; + } + aarq.calling_ae_qualifier = value; + saw_calling_ae = true; + break; + } + case 30: + if (saw_user_information || !decode_external(field, aarq.user_information)) { + aarq = {}; + return false; + } + saw_user_information = true; + break; + default: + aarq = {}; + return false; + } + } + + if (!saw_application || !saw_user_information) { + aarq = {}; + return false; + } + return true; +} + +bool AcseSpanCodec::try_decode_association_request_view( + const std::span bytes, + AssociationRequestView& request) noexcept { + request = {}; + if (!osi::SessionSpanCodec::try_decode_view(bytes, request.session) || + request.session.kind != osi::SessionWireKind::connect || + !osi::PresentationSpanCodec::try_decode_cp_view( + request.session.user_data, request.presentation) || + !try_decode_aarq_view( + request.presentation.user_data.single_asn1_type, request.aarq)) { + request = {}; + return false; + } + + if (!request.presentation.try_context_id_for_abstract_syntax( + osi::PresentationSpanCodec::acse_abstract_syntax_name(), + request.acse_presentation_context_id) || + !request.presentation.try_context_id_for_abstract_syntax( + osi::PresentationSpanCodec::mms_abstract_syntax_name(), + request.mms_presentation_context_id) || + !request_is_valid_for_response(request)) { + request = {}; + return false; + } + return true; +} + +wire::EncodeResult AcseSpanCodec::encode_default_accept_aare_into( + const std::span destination) noexcept { + if (destination.size() < kDefaultAcceptAare.size()) { + return { + wire::EncodeStatus::buffer_too_small, + 0U, + kDefaultAcceptAare.size()}; + } + std::copy( + kDefaultAcceptAare.begin(), + kDefaultAcceptAare.end(), + destination.begin()); + return { + wire::EncodeStatus::ok, + kDefaultAcceptAare.size(), + kDefaultAcceptAare.size()}; +} + +std::optional AcseSpanCodec::accept_response_size( + const AssociationRequestView& request) noexcept { + if (!request_is_valid_for_response(request)) { + return std::nullopt; + } + + const auto cpa_size = osi::PresentationSpanCodec::cpa_accepting_size( + request.presentation, default_accept_aare_size()); + if (!cpa_size || *cpa_size > std::numeric_limits::max()) { + return std::nullopt; + } + + const auto user_parameter_bytes = 2U + *cpa_size; + if (user_parameter_bytes > std::numeric_limits::max() || + request.session.parameter_bytes.size() > + std::numeric_limits::max() - user_parameter_bytes) { + return std::nullopt; + } + const auto body_bytes = request.session.parameter_bytes.size() + user_parameter_bytes; + return body_bytes + 2U; +} + +wire::EncodeResult AcseSpanCodec::build_accept_response_into( + const AssociationRequestView& request, + const std::span destination) noexcept { + return build_accept_response_into(request, kDefaultMmsInitiateResponse, destination); +} + +wire::EncodeResult AcseSpanCodec::build_accept_response_into( + const AssociationRequestView& request, + const std::span mms_initiate_response, + const std::span destination) noexcept { + const auto aare_size = accept_aare_size(mms_initiate_response.size()); + if (!aare_size || !request_is_valid_for_response(request)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto cpa_size = osi::PresentationSpanCodec::cpa_accepting_size( + request.presentation, *aare_size); + if (!cpa_size || *cpa_size > std::numeric_limits::max()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto user_parameter_bytes = 2U + *cpa_size; + if (user_parameter_bytes > std::numeric_limits::max() || + request.session.parameter_bytes.size() > + std::numeric_limits::max() - user_parameter_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto required = + request.session.parameter_bytes.size() + user_parameter_bytes + 2U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + std::array aare{}; + const auto aare_result = encode_accept_aare_into(mms_initiate_response, aare); + if (!aare_result.success() || aare_result.bytes_written != *aare_size) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + + const auto body_bytes = required - 2U; + destination[0] = osi::SessionSpanCodec::accept_code; + destination[1] = static_cast(body_bytes); + std::copy( + request.session.parameter_bytes.begin(), + request.session.parameter_bytes.end(), + destination.begin() + 2); + + std::size_t offset = 2U + request.session.parameter_bytes.size(); + destination[offset++] = osi::SessionSpanCodec::user_data_parameter; + destination[offset++] = static_cast(*cpa_size); + + const auto cpa_result = osi::PresentationSpanCodec::encode_cpa_accepting_into( + request.presentation, + std::span{aare}.first(aare_result.bytes_written), + destination.subspan(offset, *cpa_size)); + if (!cpa_result.success() || cpa_result.bytes_written != *cpa_size) { + return {cpa_result.status, 0U, required}; + } + offset += cpa_result.bytes_written; + + if (offset != required) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + return {wire::EncodeStatus::ok, required, required}; +} + +} // namespace ar::iec61850::acse diff --git a/src/asn1/ber_span_reader.cpp b/src/asn1/ber_span_reader.cpp new file mode 100644 index 000000000..65b4e5aa6 --- /dev/null +++ b/src/asn1/ber_span_reader.cpp @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/asn1/ber_span_reader.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::asn1 { + +bool BerSpanReader::try_read_tlv( + const std::span source, + std::size_t& offset, + BerTlvView& tlv) noexcept { + tlv = {}; + if (offset >= source.size()) { + return false; + } + + auto cursor = offset; + const auto start = cursor; + const auto encoded_tag = source[cursor++]; + std::int32_t tag_number = static_cast(encoded_tag & 0x1FU); + + if (tag_number == 0x1F) { + tag_number = 0; + bool read_any = false; + bool terminated = false; + while (cursor < source.size()) { + const auto byte = source[cursor++]; + read_any = true; + if (tag_number > (1'000'000 >> 7)) { + return false; + } + const auto next_tag = + (static_cast(tag_number) << 7U) | + static_cast(byte & 0x7FU); + tag_number = static_cast(next_tag); + if (tag_number > 1'000'000) { + return false; + } + if ((byte & 0x80U) == 0U) { + terminated = true; + break; + } + } + if (!read_any || !terminated) { + return false; + } + } + + if (cursor >= source.size()) { + return false; + } + + const auto length_byte = source[cursor++]; + std::size_t length = 0U; + if ((length_byte & 0x80U) == 0U) { + length = static_cast(length_byte); + } else { + const auto length_bytes = static_cast(length_byte & 0x7FU); + if (length_bytes == 0U || length_bytes > 4U || + length_bytes > source.size() - cursor) { + return false; + } + for (std::size_t index = 0U; index < length_bytes; ++index) { + length = (length << 8U) | + static_cast(source[cursor++]); + } + } + + if (length > source.size() - cursor) { + return false; + } + + const auto header_bytes = cursor - start; + const auto encoded_bytes = header_bytes + length; + tlv.encoded_tag = encoded_tag; + tlv.tag_class = static_cast((encoded_tag >> 6U) & 0x03U); + tlv.constructed = (encoded_tag & 0x20U) != 0U; + tlv.tag_number = tag_number; + tlv.header_bytes = header_bytes; + tlv.encoded_bytes = encoded_bytes; + tlv.value = source.subspan(cursor, length); + offset = cursor + length; + return true; +} + +bool BerSpanReader::try_read_exact( + const std::span source, + BerTlvView& tlv) noexcept { + std::size_t offset = 0U; + return try_read_tlv(source, offset, tlv) && offset == source.size(); +} + +std::optional BerSpanReader::read_unsigned_integer( + const BerTlvView& tlv) noexcept { + if (tlv.value.size() > sizeof(std::uint64_t)) { + return std::nullopt; + } + + std::uint64_t value = 0U; + for (const auto byte : tlv.value) { + value = (value << 8U) | static_cast(byte); + } + return value; +} + +std::optional BerSpanReader::read_signed_integer( + const BerTlvView& tlv) noexcept { + if (tlv.value.empty()) { + return std::int64_t{0}; + } + if (tlv.value.size() > sizeof(std::int64_t)) { + return std::nullopt; + } + + std::uint64_t raw = 0U; + for (const auto byte : tlv.value) { + raw = (raw << 8U) | static_cast(byte); + } + if ((tlv.value.front() & 0x80U) != 0U && + tlv.value.size() < sizeof(std::int64_t)) { + const auto used_bits = static_cast(tlv.value.size() * 8U); + raw |= (~std::uint64_t{0}) << used_bits; + } + return std::bit_cast(raw); +} + +std::optional BerSpanReader::read_uint32( + const BerTlvView& tlv) noexcept { + const auto value = read_unsigned_integer(tlv); + if (!value || *value > std::numeric_limits::max()) { + return std::nullopt; + } + return static_cast(*value); +} + +} // namespace ar::iec61850::asn1 diff --git a/src/control/mms_association_control_transport.cpp b/src/control/mms_association_control_transport.cpp index 16b1ada5f..865bcddb8 100644 --- a/src/control/mms_association_control_transport.cpp +++ b/src/control/mms_association_control_transport.cpp @@ -18,9 +18,11 @@ std::atomic g_next_control_association_id{1U}; [[nodiscard]] std::span response_payload( const mms::MmsConfirmedExchangeResult& exchange) noexcept { - return exchange.presentation_payload.empty() - ? std::span{exchange.envelope.mms_payload} - : std::span{exchange.presentation_payload}; + // exchange_confirmed() has already decoded, invoke-routed, and retained the + // canonical MMS PDU in the envelope. Always consume that payload here so + // control Read/Write decoding is independent of whether the peer returned + // a bare Presentation P-DATA unit or a compatibility session wrapper. + return exchange.envelope.mms_payload; } [[nodiscard]] std::uint64_t allocate_association_id() noexcept { @@ -61,6 +63,11 @@ std::optional MmsAssociationControlTransport::read( if (exchange.envelope.kind == mms::MmsPduKind::confirmed_error) { return std::nullopt; } + if (exchange.envelope.kind != mms::MmsPduKind::confirmed_response) { + throw std::runtime_error( + "MMS Read rejected for " + object.reference() + " (PDU kind=" + + std::to_string(static_cast(exchange.envelope.kind)) + ")."); + } const auto response = mms::MmsServiceCodec::decode_read_response( response_payload(exchange), invoke_id); if (response.results.size() != 1U || !response.results.front().success()) { @@ -87,6 +94,12 @@ MmsAssociationControlTransport::variable_specification( if (exchange.envelope.kind == mms::MmsPduKind::confirmed_error) { return std::nullopt; } + if (exchange.envelope.kind != mms::MmsPduKind::confirmed_response) { + throw std::runtime_error( + "GetVariableAccessAttributes rejected for " + object.reference() + + " (PDU kind=" + + std::to_string(static_cast(exchange.envelope.kind)) + ")."); + } const auto response = mms::MmsServiceCodec::decode_variable_access_attributes_response( response_payload(exchange), invoke_id); diff --git a/src/mms/buffered_selective_information_report_span.cpp b/src/mms/buffered_selective_information_report_span.cpp new file mode 100644 index 000000000..621a5c34f --- /dev/null +++ b/src/mms/buffered_selective_information_report_span.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/buffered_selective_information_report_span.hpp" +#include "ariec61850/mms/buffered_selective_report_detail.hpp" + +#include "ariec61850/asn1/ber_span_writer.hpp" +#include "ariec61850/mms/pdu_span.hpp" + +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool validate_report( + const MmsBufferedSelectiveInformationReportSnapshotInput& report) noexcept { + using namespace detail; + if (!visible_ascii(report.report_id, MmsInformationReportSpanCodec::maximum_report_id_bytes) || + report.optional_fields.size() != MmsInformationReportSpanCodec::optional_field_bytes || + (report.optional_fields[0] & 0x80U) != 0U || + (report.optional_fields[1] & 0x7FU) != 0U || + report.data_set_member_count == 0U || + report.data_set_member_count > MmsInformationReportSpanCodec::maximum_members || + report.included_member_indices.empty() || + report.included_member_indices.size() > report.data_set_member_count || + report.included_member_references.size() != report.included_member_indices.size() || + report.included_member_results.size() != report.included_member_indices.size() || + !reference_content_size(report.data_set_reference)) { + return false; + } + if (optional_selected(report, 0U, opt_report_timestamp) && + report.report_time.size() != MmsInformationReportSpanCodec::binary_time_bytes) { + return false; + } + if (optional_selected(report, 0U, opt_entry_id)) { + if (report.entry_id.size() != MmsInformationReportSpanCodec::entry_id_bytes) return false; + } else if (!report.entry_id.empty()) { + return false; + } + if (optional_selected(report, 0U, opt_reason_for_inclusion)) { + if (report.included_reason_for_inclusion.size() != + report.included_member_indices.size()) return false; + for (const auto reason : report.included_reason_for_inclusion) { + if (reason == 0U || (reason & 0x03U) != 0U) return false; + } + } else if (!report.included_reason_for_inclusion.empty()) { + return false; + } + + std::size_t previous = 0U; + bool first = true; + for (const auto index : report.included_member_indices) { + if (index >= report.data_set_member_count || (!first && index <= previous)) return false; + previous = index; + first = false; + } + for (const auto& reference : report.included_member_references) { + if (!reference_content_size(reference)) return false; + } + for (const auto& result : report.included_member_results) { + if (!access_result_size(result)) return false; + } + return true; +} + +[[nodiscard]] std::optional access_content_size( + const MmsBufferedSelectiveInformationReportSnapshotInput& report) noexcept { + using namespace detail; + std::size_t total = 0U; + if (!add_to(total, asn1::BerSpanWriter::tlv_size(10, report.report_id.size())) || + !add_to(total, asn1::BerSpanWriter::tlv_size(4, 1U + report.optional_fields.size()))) { + return std::nullopt; + } + if (optional_selected(report, 0U, opt_sequence_number) && + !add_to(total, asn1::BerSpanWriter::tlv_size( + 6, minimal_unsigned_size(report.sequence_number)))) return std::nullopt; + if (optional_selected(report, 0U, opt_report_timestamp) && + !add_to(total, asn1::BerSpanWriter::tlv_size(12, report.report_time.size()))) { + return std::nullopt; + } + if (optional_selected(report, 0U, opt_data_set_name)) { + const auto content = reference_content_size(report.data_set_reference); + if (!content || !add_to(total, asn1::BerSpanWriter::tlv_size(10, *content))) { + return std::nullopt; + } + } + if (optional_selected(report, 0U, opt_buffer_overflow) && + !add_to(total, asn1::BerSpanWriter::tlv_size(3, 1U))) return std::nullopt; + if (optional_selected(report, 0U, opt_entry_id) && + !add_to(total, asn1::BerSpanWriter::tlv_size(9, report.entry_id.size()))) { + return std::nullopt; + } + if (optional_selected(report, 1U, opt_conf_revision) && + !add_to(total, asn1::BerSpanWriter::tlv_size( + 6, minimal_unsigned_size(report.conf_revision)))) return std::nullopt; + + const auto inclusion_bytes = (report.data_set_member_count + 7U) / 8U; + if (!add_to(total, asn1::BerSpanWriter::tlv_size(4, 1U + inclusion_bytes))) { + return std::nullopt; + } + if (optional_selected(report, 0U, opt_data_reference)) { + for (const auto& reference : report.included_member_references) { + const auto content = reference_content_size(reference); + if (!content || !add_to(total, asn1::BerSpanWriter::tlv_size(10, *content))) { + return std::nullopt; + } + } + } + for (const auto& result : report.included_member_results) { + if (!add_to(total, access_result_size(result))) return std::nullopt; + } + if (optional_selected(report, 0U, opt_reason_for_inclusion)) { + const auto reason_tlv = asn1::BerSpanWriter::tlv_size(4, 2U); + for (std::size_t index = 0U; index < report.included_member_indices.size(); ++index) { + if (!add_to(total, reason_tlv)) return std::nullopt; + } + } + return total; +} + +[[nodiscard]] bool write_inclusion( + asn1::BerSpanWriter& writer, + const std::size_t member_count, + const std::span included) noexcept { + const auto byte_count = (member_count + 7U) / 8U; + const auto unused = static_cast((byte_count * 8U) - member_count); + if (!writer.write_tlv_header(asn1::BerClass::context_specific, false, 4, 1U + byte_count) || + !writer.write_byte(unused)) return false; + + std::size_t included_offset = 0U; + for (std::size_t byte_index = 0U; byte_index < byte_count; ++byte_index) { + std::uint8_t value = 0U; + while (included_offset < included.size() && included[included_offset] / 8U == byte_index) { + const auto bit = included[included_offset] % 8U; + value = static_cast(value | static_cast(0x80U >> bit)); + ++included_offset; + } + if (!writer.write_byte(value)) return false; + } + return included_offset == included.size(); +} + +} // namespace + +wire::EncodeResult MmsBufferedSelectiveInformationReportSpanCodec::encode_snapshot_into( + const MmsBufferedSelectiveInformationReportSnapshotInput& report, + const std::span destination) noexcept { + using namespace detail; + if (!validate_report(report)) return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + + const auto access_content = access_content_size(report); + const auto access_tlv = access_content ? asn1::BerSpanWriter::tlv_size(0, *access_content) : std::nullopt; + const auto vmd_name_tlv = asn1::BerSpanWriter::tlv_size(0, 3U); + const auto variable_access_tlv = vmd_name_tlv ? asn1::BerSpanWriter::tlv_size(1, *vmd_name_tlv) : std::nullopt; + std::optional information_content; + if (access_tlv && variable_access_tlv && + *access_tlv <= std::numeric_limits::max() - *variable_access_tlv) { + information_content = *variable_access_tlv + *access_tlv; + } + const auto service_tlv = information_content ? asn1::BerSpanWriter::tlv_size(0, *information_content) : std::nullopt; + const auto required = service_tlv ? asn1::BerSpanWriter::tlv_size(3, *service_tlv) : std::nullopt; + if (!access_content || !access_tlv || !vmd_name_tlv || !variable_access_tlv || + !information_content || !service_tlv || !required || + *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header(asn1::BerClass::context_specific, true, 3, *service_tlv) || + !writer.write_tlv_header(asn1::BerClass::context_specific, true, 0, *information_content) || + !writer.write_tlv_header(asn1::BerClass::context_specific, true, 1, *vmd_name_tlv) || + !writer.write_tlv_header(asn1::BerClass::context_specific, false, 0, 3U) || + !writer.write_bytes(std::span{ + reinterpret_cast("RPT"), 3U}) || + !writer.write_tlv_header(asn1::BerClass::context_specific, true, 0, *access_content) || + !write_visible(writer, report.report_id) || + !write_bit_string(writer, 6U, report.optional_fields)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + + if (optional_selected(report, 0U, opt_sequence_number) && + !write_unsigned(writer, report.sequence_number)) return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + if (optional_selected(report, 0U, opt_report_timestamp) && + (!writer.write_tlv_header(asn1::BerClass::context_specific, false, 12, report.report_time.size()) || + !writer.write_bytes(report.report_time))) return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + if (optional_selected(report, 0U, opt_data_set_name) && !write_reference(writer, report.data_set_reference)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, opt_buffer_overflow) && !write_boolean(writer, report.buffer_overflow)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, opt_entry_id) && + (!writer.write_tlv_header(asn1::BerClass::context_specific, false, 9, report.entry_id.size()) || + !writer.write_bytes(report.entry_id))) return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + if (optional_selected(report, 1U, opt_conf_revision) && !write_unsigned(writer, report.conf_revision)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (!write_inclusion(writer, report.data_set_member_count, report.included_member_indices)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, opt_data_reference)) { + for (const auto& reference : report.included_member_references) { + if (!write_reference(writer, reference)) return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + for (const auto& result : report.included_member_results) { + if (!write_access_result(writer, result)) return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, opt_reason_for_inclusion)) { + for (const auto reason : report.included_reason_for_inclusion) { + if (!write_bit_string(writer, 2U, std::span{&reason, 1U})) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + } + if (writer.size() != *required) return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + return {wire::EncodeStatus::ok, *required, *required}; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/connection_runtime.cpp b/src/mms/connection_runtime.cpp new file mode 100644 index 000000000..8d3caf4d0 --- /dev/null +++ b/src/mms/connection_runtime.cpp @@ -0,0 +1,682 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/connection_runtime.hpp" + +#include "ariec61850/acse/association_span.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" +#include "ariec61850/mms/pdu_span.hpp" +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +// Must remain aligned with MmsPduSpanCodec::encode_default_initiate_response_into(). +constexpr std::uint32_t kServerMaximumMmsPduSize = 65'000U; +constexpr std::uint32_t kServerMaximumOutstandingCalling = 10U; +constexpr std::uint32_t kServerMaximumOutstandingCalled = 10U; +constexpr std::uint32_t kServerMaximumNestingLevel = 5U; + +[[nodiscard]] MmsStaticConnectionResult make_result( + const MmsStaticConnectionStatus status, + const MmsStaticConnectionState state, + const std::size_t consumed = 0U, + const std::size_t written = 0U) noexcept { + MmsStaticConnectionResult result; + result.status = status; + result.state = state; + result.consumed_bytes = consumed; + result.bytes_written = written; + return result; +} + +[[nodiscard]] MmsStaticConnectionResult make_response_capacity( + const MmsStaticConnectionState state, + const std::size_t required) noexcept { + auto result = make_result( + MmsStaticConnectionStatus::response_buffer_too_small, state); + result.required_response_bytes = required; + return result; +} + +[[nodiscard]] MmsStaticConnectionResult make_workspace_capacity( + const MmsStaticConnectionState state, + const std::size_t required) noexcept { + auto result = make_result( + MmsStaticConnectionStatus::workspace_too_small, state); + result.required_workspace_bytes = required; + return result; +} + +[[nodiscard]] bool add_overhead( + const std::size_t base, + const std::size_t overhead, + std::size_t& total) noexcept { + if (base > std::numeric_limits::max() - overhead) { + total = 0U; + return false; + } + total = base + overhead; + return true; +} + +[[nodiscard]] bool selected_tpdu_size_bytes( + const osi::CotpTpduView& request, + const std::uint8_t local_maximum_code, + std::size_t& selected_bytes) noexcept { + selected_bytes = 0U; + std::size_t local_bytes{}; + if (!osi::CotpSpanCodec::try_tpdu_size_bytes(local_maximum_code, local_bytes)) { + return false; + } + + std::span offered; + if (!request.try_parameter(osi::CotpSpanCodec::tpdu_size_parameter, offered)) { + selected_bytes = local_bytes; + return true; + } + if (offered.size() != 1U) { + return false; + } + + std::size_t peer_bytes{}; + if (!osi::CotpSpanCodec::try_tpdu_size_bytes(offered[0], peer_bytes)) { + return false; + } + selected_bytes = std::min(local_bytes, peer_bytes); + return true; +} + +[[nodiscard]] MmsStaticConnectionResult wrap_cotp_data_response( + const std::span session_or_presentation, + const std::size_t consumed, + const MmsStaticConnectionState state, + const std::size_t negotiated_tpdu_size_bytes, + const std::span response, + const std::span workspace) noexcept { + std::size_t cotp_required{}; + if (!add_overhead(session_or_presentation.size(), 3U, cotp_required)) { + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + if (negotiated_tpdu_size_bytes == 0U || + cotp_required > negotiated_tpdu_size_bytes) { + return make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, state, consumed); + } + + std::size_t required{}; + if (!add_overhead(cotp_required, osi::TpktSpanCodec::header_length, required)) { + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + if (response.size() < required) { + return make_response_capacity(state, required); + } + if (workspace.size() < required) { + return make_workspace_capacity(state, required); + } + + const auto cotp = osi::CotpSpanCodec::encode_data_into( + session_or_presentation, + response.first(required - osi::TpktSpanCodec::header_length)); + if (!cotp.success()) { + if (cotp.status == wire::EncodeStatus::buffer_too_small) { + return make_response_capacity( + state, cotp.required_bytes + osi::TpktSpanCodec::header_length); + } + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + + const auto tpkt = osi::TpktSpanCodec::encode_into( + response.first(cotp.bytes_written), workspace.first(required)); + if (!tpkt.success() || tpkt.bytes_written != required) { + if (tpkt.status == wire::EncodeStatus::buffer_too_small) { + return make_workspace_capacity(state, tpkt.required_bytes); + } + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + std::copy_n(workspace.begin(), tpkt.bytes_written, response.begin()); + return make_result( + MmsStaticConnectionStatus::response_ready, + state, + consumed, + tpkt.bytes_written); +} + +[[nodiscard]] MmsConfirmedRequestRejectReason reject_reason_for( + const MmsStaticDispatchStatus status) noexcept { + switch (status) { + case MmsStaticDispatchStatus::unsupported_service: + return MmsConfirmedRequestRejectReason::unrecognized_service; + case MmsStaticDispatchStatus::malformed_request: + case MmsStaticDispatchStatus::unsupported_request: + return MmsConfirmedRequestRejectReason::invalid_argument; + case MmsStaticDispatchStatus::object_not_found: + case MmsStaticDispatchStatus::response_ready: + case MmsStaticDispatchStatus::invalid_object_table: + case MmsStaticDispatchStatus::workspace_too_small: + case MmsStaticDispatchStatus::response_buffer_too_small: + case MmsStaticDispatchStatus::backend_failure: + return MmsConfirmedRequestRejectReason::other; + } + return MmsConfirmedRequestRejectReason::other; +} + +[[nodiscard]] std::size_t positive_integer_size(const std::uint32_t value) noexcept { + std::size_t bytes = 1U; + auto remaining = value; + while (remaining > 0xFFU) { + ++bytes; + remaining >>= 8U; + } + const auto leading = static_cast( + value >> static_cast((bytes - 1U) * 8U)); + return bytes + ((leading & 0x80U) != 0U ? 1U : 0U); +} + +[[nodiscard]] bool write_positive_integer( + asn1::BerSpanWriter& writer, + const std::uint32_t value, + const std::size_t bytes) noexcept { + const auto encoded_bytes = bytes - + ((bytes > 1U && + (value >> static_cast((bytes - 2U) * 8U)) <= 0xFFU) + ? 1U + : 0U); + if (encoded_bytes < bytes && !writer.write_byte(0U)) { + return false; + } + for (std::size_t index = encoded_bytes; index > 0U; --index) { + const auto shift = static_cast((index - 1U) * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] wire::EncodeResult encode_object_not_found_confirmed_error( + const std::uint32_t invoke_id, + const std::span destination) noexcept { + if (invoke_id > MmsPduSpanCodec::maximum_invoke_id) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + // ConfirmedErrorPDU ::= [2] { invokeID [0], serviceError [2] } + // serviceError.errorClass uses access[7] object-non-existent(2). + const auto invoke_bytes = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(0, invoke_bytes); + const auto access_tlv = asn1::BerSpanWriter::tlv_size(7, 1U); + if (!invoke_tlv || !access_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto error_class_tlv = asn1::BerSpanWriter::tlv_size(0, *access_tlv); + if (!error_class_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto service_error_tlv = asn1::BerSpanWriter::tlv_size(2, *error_class_tlv); + if (!service_error_tlv || + *invoke_tlv > std::numeric_limits::max() - *service_error_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content = *invoke_tlv + *service_error_tlv; + const auto required = asn1::BerSpanWriter::tlv_size(2, content); + if (!required) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header(asn1::BerClass::context_specific, true, 2, content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, invoke_bytes) || + !write_positive_integer(writer, invoke_id, invoke_bytes) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 2, *error_class_tlv) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 0, *access_tlv) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 7, 1U) || + !writer.write_byte(2U) || writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +[[nodiscard]] bool write_outer_capacity( + const MmsConfirmedPduView& confirmed, + const MmsStaticApplicationDispatcher& dispatcher, + const std::uint32_t presentation_context_id, + std::size_t& mms_required, + std::size_t& frame_required) noexcept { + mms_required = 0U; + frame_required = 0U; + if (confirmed.service() != MmsWireConfirmedService::write) { + return false; + } + + MmsWriteRequestView write; + if (!MmsServiceSpanCodec::try_decode_write_request(confirmed, write) || + write.variable_count == 0U || + write.variable_count > dispatcher.policy().maximum_write_variables) { + return false; + } + + std::array worst{}; + for (std::size_t index = 0U; index < write.variable_count; ++index) { + worst[index] = MmsWriteAccessResultInput{ + false, + std::numeric_limits::max()}; + } + const auto probe = MmsServiceSpanCodec::encode_write_response_into( + confirmed.invoke_id, + std::span{worst}.first(write.variable_count), + {}); + if (probe.status != wire::EncodeStatus::buffer_too_small || + probe.required_bytes == 0U) { + return false; + } + mms_required = probe.required_bytes; + const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( + presentation_context_id, probe.required_bytes); + return fully_encoded && add_overhead(*fully_encoded, 11U, frame_required); +} + +} // namespace + +void MmsStaticConnectionRuntime::notify_association_closed() noexcept { + if (!association_active_ || association_close_notified_) { + return; + } + + association_close_notified_ = true; + association_active_ = false; + if (policy_.association_closed == nullptr || policy_.association_id == 0U) { + return; + } + + const auto now = policy_.now_ms == nullptr + ? std::uint64_t{0U} + : policy_.now_ms(policy_.now_context); + policy_.association_closed( + policy_.association_closed_context, + policy_.association_id, + now); +} + +void MmsStaticConnectionRuntime::close_transport() noexcept { + notify_association_closed(); + state_ = MmsStaticConnectionState::closed; + mms_presentation_context_id_ = 0U; + negotiated_tpdu_size_bytes_ = 0U; + negotiated_mms_pdu_size_ = 0U; +} + +void MmsStaticConnectionRuntime::reset() noexcept { + notify_association_closed(); + state_ = MmsStaticConnectionState::awaiting_cotp_connect; + mms_presentation_context_id_ = 0U; + negotiated_tpdu_size_bytes_ = 0U; + negotiated_mms_pdu_size_ = 0U; + association_active_ = false; + association_close_notified_ = false; +} + +MmsStaticConnectionResult MmsStaticConnectionRuntime::process_tcp_window( + const std::span tcp_bytes, + const std::span response, + const std::span workspace) noexcept { + if (state_ == MmsStaticConnectionState::closed) { + return make_result(MmsStaticConnectionStatus::closed, state_); + } + if (state_ == MmsStaticConnectionState::fault) { + return make_result(MmsStaticConnectionStatus::protocol_violation, state_); + } + + const auto peek = osi::TpktSpanCodec::peek_frame(tcp_bytes); + if (peek.status == osi::TpktPeekStatus::need_more) { + return make_result(MmsStaticConnectionStatus::need_more, state_); + } + if (peek.status != osi::TpktPeekStatus::ready || peek.frame_bytes == 0U) { + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::malformed_transport, state_); + } + + const auto frame_bytes = tcp_bytes.first(peek.frame_bytes); + osi::TpktFrameView tpkt; + osi::CotpTpduView cotp; + if (!osi::TpktSpanCodec::try_decode_view(frame_bytes, tpkt) || + !osi::CotpSpanCodec::try_decode_view(tpkt.payload, cotp)) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::malformed_transport, state_, peek.frame_bytes); + } + + if (cotp.kind == osi::CotpWireKind::disconnect_request) { + close_transport(); + return make_result( + MmsStaticConnectionStatus::closed, state_, peek.frame_bytes); + } + + if (state_ == MmsStaticConnectionState::awaiting_cotp_connect) { + if (cotp.kind != osi::CotpWireKind::connection_request) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + std::size_t selected_tpdu_bytes{}; + if (!selected_tpdu_size_bytes( + cotp, policy_.maximum_tpdu_size_code, selected_tpdu_bytes)) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + const auto cc = osi::CotpSpanCodec::encode_connection_confirm_from_request_into( + cotp, + policy_.cotp_source_reference, + workspace, + policy_.maximum_tpdu_size_code); + if (!cc.success()) { + if (cc.status == wire::EncodeStatus::buffer_too_small) { + return make_workspace_capacity(state_, cc.required_bytes); + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + std::size_t required{}; + if (!add_overhead(cc.bytes_written, osi::TpktSpanCodec::header_length, required)) { + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + if (response.size() < required) { + return make_response_capacity(state_, required); + } + const auto encoded = osi::TpktSpanCodec::encode_into( + workspace.first(cc.bytes_written), response.first(required)); + if (!encoded.success()) { + if (encoded.status == wire::EncodeStatus::buffer_too_small) { + return make_response_capacity(state_, encoded.required_bytes); + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + negotiated_tpdu_size_bytes_ = selected_tpdu_bytes; + state_ = MmsStaticConnectionState::awaiting_association; + return make_result( + MmsStaticConnectionStatus::response_ready, + state_, + peek.frame_bytes, + encoded.bytes_written); + } + + if (cotp.kind != osi::CotpWireKind::data || + (policy_.require_end_of_transmission && !cotp.end_of_transmission)) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + if (state_ == MmsStaticConnectionState::awaiting_association) { + acse::AssociationRequestView association; + if (!acse::AcseSpanCodec::try_decode_association_request_view( + cotp.user_data, association)) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + MmsInitiateView initiate; + if (!MmsPduSpanCodec::try_decode_initiate_request_view( + association.aarq.user_information.single_asn1_type, initiate) || + association.mms_presentation_context_id == 0U) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + const auto selected_mms_pdu_size = std::min( + initiate.maximum_mms_pdu_size, kServerMaximumMmsPduSize); + + const auto selected_outstanding_calling = std::min( + initiate.maximum_outstanding_calling, + kServerMaximumOutstandingCalling); + const auto selected_outstanding_called = std::min( + initiate.maximum_outstanding_called, + kServerMaximumOutstandingCalled); + const auto selected_nesting_level = std::min( + initiate.data_structure_nesting_level, + kServerMaximumNestingLevel); + std::array initiate_response{}; + const auto initiate_encoded = MmsPduSpanCodec::encode_initiate_response_into( + selected_mms_pdu_size, + selected_outstanding_calling, + selected_outstanding_called, + selected_nesting_level, + initiate_response); + if (!initiate_encoded.success()) { + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + const auto accept = acse::AcseSpanCodec::build_accept_response_into( + association, + std::span{initiate_response}.first( + initiate_encoded.bytes_written), + workspace); + if (!accept.success()) { + if (accept.status == wire::EncodeStatus::buffer_too_small) { + return make_workspace_capacity(state_, accept.required_bytes); + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + const auto wrapped = wrap_cotp_data_response( + workspace.first(accept.bytes_written), + peek.frame_bytes, + MmsStaticConnectionState::established, + negotiated_tpdu_size_bytes_, + response, + workspace); + if (!wrapped.response_ready()) { + return wrapped; + } + mms_presentation_context_id_ = association.mms_presentation_context_id; + negotiated_mms_pdu_size_ = selected_mms_pdu_size; + state_ = MmsStaticConnectionState::established; + association_active_ = true; + association_close_notified_ = false; + auto result = wrapped; + result.state = state_; + return result; + } + + if (state_ != MmsStaticConnectionState::established) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + osi::SessionDataTransferView session; + osi::PresentationPdvView pdv; + if (!osi::SessionSpanCodec::try_decode_data_transfer_view(cotp.user_data, session) || + !osi::PresentationSpanCodec::try_decode_fully_encoded_data_view( + session.presentation_payload, pdv) || + pdv.context_id != mms_presentation_context_id_) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + MmsConfirmedPduView confirmed; + const bool is_confirmed_request = MmsPduSpanCodec::try_decode_confirmed_request_view( + pdv.single_asn1_type, confirmed); + if (is_confirmed_request) { + std::size_t write_mms_required{}; + std::size_t write_frame_required{}; + if (write_outer_capacity( + confirmed, + dispatcher_, + mms_presentation_context_id_, + write_mms_required, + write_frame_required)) { + if (negotiated_mms_pdu_size_ == 0U || + write_mms_required > negotiated_mms_pdu_size_ || + write_frame_required < osi::TpktSpanCodec::header_length || + write_frame_required - osi::TpktSpanCodec::header_length > + negotiated_tpdu_size_bytes_) { + return make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes); + } + if (response.size() < write_frame_required) { + return make_response_capacity(state_, write_frame_required); + } + if (workspace.size() < write_frame_required) { + return make_workspace_capacity(state_, write_frame_required); + } + } + } + + const auto application = dispatcher_.dispatch( + pdv.single_asn1_type, response, workspace, policy_.access_context()); + std::size_t mms_response_bytes = application.bytes_written; + if (!application.success()) { + if (application.status == MmsStaticDispatchStatus::response_buffer_too_small) { + const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( + mms_presentation_context_id_, application.required_bytes); + std::size_t required{}; + if (fully_encoded && add_overhead(*fully_encoded, 11U, required)) { + auto result = make_response_capacity(state_, required); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + if (application.status == MmsStaticDispatchStatus::workspace_too_small) { + auto result = make_workspace_capacity(state_, application.required_bytes); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; + } + if (application.status == MmsStaticDispatchStatus::backend_failure || + application.status == MmsStaticDispatchStatus::invalid_object_table) { + state_ = MmsStaticConnectionState::fault; + auto result = make_result( + MmsStaticConnectionStatus::backend_failure, + state_, + peek.frame_bytes); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; + } + if (!is_confirmed_request) { + auto result = make_result( + MmsStaticConnectionStatus::application_rejected, + state_, + peek.frame_bytes); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; + } + + const auto rejected = application.status == MmsStaticDispatchStatus::object_not_found + ? encode_object_not_found_confirmed_error(confirmed.invoke_id, response) + : MmsPduSpanCodec::encode_confirmed_request_reject_into( + confirmed.invoke_id, + reject_reason_for(application.status), + response); + if (!rejected.success()) { + if (rejected.status == wire::EncodeStatus::buffer_too_small) { + const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( + mms_presentation_context_id_, rejected.required_bytes); + std::size_t required{}; + if (fully_encoded && add_overhead(*fully_encoded, 11U, required)) { + auto result = make_response_capacity(state_, required); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = confirmed.invoke_id; + return result; + } + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + mms_response_bytes = rejected.bytes_written; + } + + if (negotiated_mms_pdu_size_ == 0U || + mms_response_bytes > negotiated_mms_pdu_size_) { + return make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes); + } + + const auto p_data = osi::PresentationSpanCodec::encode_p_data_into( + response.first(mms_response_bytes), + workspace, + mms_presentation_context_id_, + true); + if (!p_data.success()) { + if (p_data.status == wire::EncodeStatus::buffer_too_small) { + std::size_t required{}; + if (add_overhead(p_data.required_bytes, 7U, required)) { + return make_workspace_capacity(state_, required); + } + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + auto wrapped = wrap_cotp_data_response( + workspace.first(p_data.bytes_written), + peek.frame_bytes, + state_, + negotiated_tpdu_size_bytes_, + response, + workspace); + if (wrapped.response_ready()) { + wrapped.application_status = application.status; + wrapped.application_service = application.service; + wrapped.invoke_id = is_confirmed_request ? confirmed.invoke_id : application.invoke_id; + } + return wrapped; +} + +} // namespace ar::iec61850::mms \ No newline at end of file diff --git a/src/mms/connection_runtime_iedsim.cpp b/src/mms/connection_runtime_iedsim.cpp new file mode 100644 index 000000000..6da4e90a8 --- /dev/null +++ b/src/mms/connection_runtime_iedsim.cpp @@ -0,0 +1,937 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/connection_runtime.hpp" + +#include "ariec61850/acse/association_span.hpp" +#include "ariec61850/mms/pdu_span.hpp" +#include "ariec61850/mms/services_span.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +// Must remain aligned with MmsPduSpanCodec::encode_default_initiate_response_into(). +constexpr std::uint32_t kServerMaximumMmsPduSize = 65'000U; +constexpr std::uint32_t kServerMaximumOutstandingCalling = 10U; +constexpr std::uint32_t kServerMaximumOutstandingCalled = 10U; +constexpr std::uint32_t kServerMaximumNestingLevel = 5U; + +constexpr std::array kConservativeStructureType{ + 0xA2U, 0x02U, 0xA1U, 0x00U}; + +// FileDirectory-Response service value: listOfDirectoryEntry [0] empty, +// moreFollows [1] FALSE. Mirrors the deterministic behavior of the proven +// ARIEC61850 engineering-client simulator without claiming file-server support. +constexpr std::array kEmptyFileDirectoryFields{ + 0xA0U, 0x00U, 0x81U, 0x01U, 0x00U}; + +[[nodiscard]] MmsStaticConnectionResult make_result( + const MmsStaticConnectionStatus status, + const MmsStaticConnectionState state, + const std::size_t consumed = 0U, + const std::size_t written = 0U) noexcept { + MmsStaticConnectionResult result; + result.status = status; + result.state = state; + result.consumed_bytes = consumed; + result.bytes_written = written; + return result; +} + +[[nodiscard]] MmsStaticConnectionResult make_response_capacity( + const MmsStaticConnectionState state, + const std::size_t required) noexcept { + auto result = make_result( + MmsStaticConnectionStatus::response_buffer_too_small, state); + result.required_response_bytes = required; + return result; +} + +[[nodiscard]] MmsStaticConnectionResult make_workspace_capacity( + const MmsStaticConnectionState state, + const std::size_t required) noexcept { + auto result = make_result( + MmsStaticConnectionStatus::workspace_too_small, state); + result.required_workspace_bytes = required; + return result; +} + +[[nodiscard]] bool add_overhead( + const std::size_t base, + const std::size_t overhead, + std::size_t& total) noexcept { + if (base > std::numeric_limits::max() - overhead) { + total = 0U; + return false; + } + total = base + overhead; + return true; +} + +[[nodiscard]] bool selected_tpdu_size_bytes( + const osi::CotpTpduView& request, + const std::uint8_t local_maximum_code, + std::size_t& selected_bytes) noexcept { + selected_bytes = 0U; + std::size_t local_bytes{}; + if (!osi::CotpSpanCodec::try_tpdu_size_bytes(local_maximum_code, local_bytes)) { + return false; + } + + std::span offered; + if (!request.try_parameter(osi::CotpSpanCodec::tpdu_size_parameter, offered)) { + selected_bytes = local_bytes; + return true; + } + if (offered.size() != 1U) { + return false; + } + + std::size_t peer_bytes{}; + if (!osi::CotpSpanCodec::try_tpdu_size_bytes(offered[0], peer_bytes)) { + return false; + } + selected_bytes = std::min(local_bytes, peer_bytes); + return true; +} + +[[nodiscard]] bool cotp_stream_size( + const std::size_t payload_bytes, + const std::size_t negotiated_tpdu_size_bytes, + std::size_t& maximum_user_data, + std::size_t& segment_count, + std::size_t& required) noexcept { + maximum_user_data = 0U; + segment_count = 0U; + required = 0U; + if (negotiated_tpdu_size_bytes <= 3U) { + return false; + } + + maximum_user_data = negotiated_tpdu_size_bytes - 3U; + segment_count = payload_bytes == 0U + ? 1U + : 1U + ((payload_bytes - 1U) / maximum_user_data); + if (segment_count > + (std::numeric_limits::max() - payload_bytes) / 7U) { + return false; + } + required = payload_bytes + segment_count * 7U; + return true; +} + +[[nodiscard]] MmsStaticConnectionResult wrap_cotp_data_response( + const std::span session_or_presentation, + const std::size_t consumed, + const MmsStaticConnectionState state, + const std::size_t negotiated_tpdu_size_bytes, + const std::span response) noexcept { + std::size_t maximum_user_data{}; + std::size_t segment_count{}; + std::size_t required{}; + if (!cotp_stream_size( + session_or_presentation.size(), + negotiated_tpdu_size_bytes, + maximum_user_data, + segment_count, + required)) { + return make_result(MmsStaticConnectionStatus::peer_limit_exceeded, state, consumed); + } + if (response.size() < required) { + return make_response_capacity(state, required); + } + + std::size_t input_offset = 0U; + std::size_t output_offset = 0U; + for (std::size_t segment = 0U; segment < segment_count; ++segment) { + const auto remaining = session_or_presentation.size() - input_offset; + const auto chunk_size = std::min(remaining, maximum_user_data); + const auto final_segment = segment + 1U == segment_count; + const auto frame_bytes = chunk_size + 7U; + if (frame_bytes > std::numeric_limits::max()) { + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + + auto frame = response.subspan(output_offset, frame_bytes); + const auto cotp = osi::CotpSpanCodec::encode_data_into( + session_or_presentation.subspan(input_offset, chunk_size), + frame.subspan(osi::TpktSpanCodec::header_length), + final_segment, + 0U); + if (!cotp.success() || cotp.bytes_written != chunk_size + 3U) { + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + + frame[0] = 0x03U; + frame[1] = 0x00U; + frame[2] = static_cast((frame_bytes >> 8U) & 0xFFU); + frame[3] = static_cast(frame_bytes & 0xFFU); + input_offset += chunk_size; + output_offset += frame_bytes; + } + + if (input_offset != session_or_presentation.size() || output_offset != required) { + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + return make_result( + MmsStaticConnectionStatus::response_ready, + state, + consumed, + required); +} + +[[nodiscard]] MmsStaticConnectionResult application_rejected( + const MmsStaticConnectionState state, + const std::size_t consumed, + const MmsStaticDispatchResult& application) noexcept { + auto result = make_result( + MmsStaticConnectionStatus::application_rejected, state, consumed); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; +} + +[[nodiscard]] MmsStaticConnectionResult wrap_mms_response( + const wire::EncodeResult encoded_mms, + const std::size_t consumed, + const MmsStaticConnectionState state, + const std::uint32_t presentation_context_id, + const std::uint32_t negotiated_mms_pdu_size, + const std::size_t negotiated_tpdu_size_bytes, + const std::span response, + const std::span workspace, + const MmsStaticDispatchStatus application_status, + const MmsWireConfirmedService service, + const std::uint32_t invoke_id) noexcept { + if (!encoded_mms.success()) { + if (encoded_mms.status == wire::EncodeStatus::buffer_too_small && + encoded_mms.required_bytes > 0U) { + if (negotiated_mms_pdu_size == 0U || + encoded_mms.required_bytes > negotiated_mms_pdu_size) { + return make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state, + consumed); + } + const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( + presentation_context_id, encoded_mms.required_bytes); + if (fully_encoded) { + std::size_t maximum_user_data{}; + std::size_t segment_count{}; + std::size_t required{}; + if (cotp_stream_size( + *fully_encoded, + negotiated_tpdu_size_bytes, + maximum_user_data, + segment_count, + required)) { + auto result = make_response_capacity(state, required); + result.application_status = application_status; + result.application_service = service; + result.invoke_id = invoke_id; + return result; + } + } + } + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + + if (negotiated_mms_pdu_size == 0U || + encoded_mms.bytes_written > negotiated_mms_pdu_size) { + return make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state, + consumed); + } + + const auto p_data = osi::PresentationSpanCodec::encode_p_data_into( + response.first(encoded_mms.bytes_written), + workspace, + presentation_context_id, + true); + if (!p_data.success()) { + if (p_data.status == wire::EncodeStatus::buffer_too_small) { + auto result = make_workspace_capacity(state, p_data.required_bytes); + result.application_status = application_status; + result.application_service = service; + result.invoke_id = invoke_id; + return result; + } + return make_result(MmsStaticConnectionStatus::backend_failure, state); + } + + auto wrapped = wrap_cotp_data_response( + workspace.first(p_data.bytes_written), + consumed, + state, + negotiated_tpdu_size_bytes, + response); + if (wrapped.response_ready()) { + wrapped.application_status = application_status; + wrapped.application_service = service; + wrapped.invoke_id = invoke_id; + } + return wrapped; +} + +[[nodiscard]] bool write_outer_capacity( + const MmsConfirmedPduView& confirmed, + const MmsStaticApplicationDispatcher& dispatcher, + const std::uint32_t presentation_context_id, + const std::size_t negotiated_tpdu_size_bytes, + std::size_t& mms_required, + std::size_t& frame_required) noexcept { + mms_required = 0U; + frame_required = 0U; + if (confirmed.service() != MmsWireConfirmedService::write) { + return false; + } + + MmsWriteRequestView write; + if (!MmsServiceSpanCodec::try_decode_write_request(confirmed, write) || + write.variable_count == 0U || + write.variable_count > dispatcher.policy().maximum_write_variables) { + return false; + } + + std::array worst{}; + for (std::size_t index = 0U; index < write.variable_count; ++index) { + worst[index] = MmsWriteAccessResultInput{ + false, + std::numeric_limits::max()}; + } + const auto probe = MmsServiceSpanCodec::encode_write_response_into( + confirmed.invoke_id, + std::span{worst}.first(write.variable_count), + {}); + if (probe.status != wire::EncodeStatus::buffer_too_small || + probe.required_bytes == 0U) { + return false; + } + mms_required = probe.required_bytes; + const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( + presentation_context_id, probe.required_bytes); + if (!fully_encoded) { + return false; + } + std::size_t maximum_user_data{}; + std::size_t segment_count{}; + return cotp_stream_size( + *fully_encoded, + negotiated_tpdu_size_bytes, + maximum_user_data, + segment_count, + frame_required); +} + +[[nodiscard]] bool compatibility_error_status( + const MmsStaticDispatchStatus status) noexcept { + return status == MmsStaticDispatchStatus::malformed_request || + status == MmsStaticDispatchStatus::unsupported_service || + status == MmsStaticDispatchStatus::unsupported_request || + status == MmsStaticDispatchStatus::object_not_found; +} + +} // namespace + +void MmsStaticConnectionRuntime::clear_cotp_reassembly() noexcept { + cotp_reassembly_size_ = 0U; + cotp_reassembly_complete_ = false; + cotp_reassembly_uses_workspace_ = false; + cotp_reassembly_workspace_data_ = nullptr; + cotp_reassembly_workspace_size_ = 0U; +} + +void MmsStaticConnectionRuntime::notify_association_closed() noexcept { + if (!association_active_ || association_close_notified_) { + return; + } + + association_close_notified_ = true; + association_active_ = false; + if (policy_.association_closed == nullptr || policy_.association_id == 0U) { + return; + } + + const auto now = policy_.now_ms == nullptr + ? std::uint64_t{0U} + : policy_.now_ms(policy_.now_context); + policy_.association_closed( + policy_.association_closed_context, + policy_.association_id, + now); +} + +void MmsStaticConnectionRuntime::close_transport() noexcept { + notify_association_closed(); + clear_cotp_reassembly(); + state_ = MmsStaticConnectionState::closed; + mms_presentation_context_id_ = 0U; + negotiated_tpdu_size_bytes_ = 0U; + negotiated_mms_pdu_size_ = 0U; +} + +void MmsStaticConnectionRuntime::reset() noexcept { + notify_association_closed(); + clear_cotp_reassembly(); + state_ = MmsStaticConnectionState::awaiting_cotp_connect; + mms_presentation_context_id_ = 0U; + negotiated_tpdu_size_bytes_ = 0U; + negotiated_mms_pdu_size_ = 0U; + association_active_ = false; + association_close_notified_ = false; +} + +MmsStaticConnectionResult MmsStaticConnectionRuntime::process_tcp_window( + const std::span tcp_bytes, + const std::span response, + const std::span workspace_storage) noexcept { + auto workspace = workspace_storage; + if (state_ == MmsStaticConnectionState::closed) { + return make_result(MmsStaticConnectionStatus::closed, state_); + } + if (state_ == MmsStaticConnectionState::fault) { + return make_result(MmsStaticConnectionStatus::protocol_violation, state_); + } + + const auto peek = osi::TpktSpanCodec::peek_frame(tcp_bytes); + if (peek.status == osi::TpktPeekStatus::need_more) { + return make_result(MmsStaticConnectionStatus::need_more, state_); + } + if (peek.status != osi::TpktPeekStatus::ready || peek.frame_bytes == 0U) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::malformed_transport, state_); + } + + const auto frame_bytes = tcp_bytes.first(peek.frame_bytes); + osi::TpktFrameView tpkt; + osi::CotpTpduView cotp; + if (!osi::TpktSpanCodec::try_decode_view(frame_bytes, tpkt) || + !osi::CotpSpanCodec::try_decode_view(tpkt.payload, cotp)) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::malformed_transport, state_, peek.frame_bytes); + } + + if (cotp.kind == osi::CotpWireKind::disconnect_request) { + close_transport(); + return make_result( + MmsStaticConnectionStatus::closed, state_, peek.frame_bytes); + } + + if (state_ == MmsStaticConnectionState::awaiting_cotp_connect) { + if (cotp.kind != osi::CotpWireKind::connection_request) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + std::size_t selected_tpdu_bytes{}; + if (!selected_tpdu_size_bytes( + cotp, policy_.maximum_tpdu_size_code, selected_tpdu_bytes)) { + state_ = MmsStaticConnectionState::fault; + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + const auto cc = osi::CotpSpanCodec::encode_connection_confirm_from_request_into( + cotp, + policy_.cotp_source_reference, + workspace, + policy_.maximum_tpdu_size_code); + if (!cc.success()) { + if (cc.status == wire::EncodeStatus::buffer_too_small) { + return make_workspace_capacity(state_, cc.required_bytes); + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + std::size_t required{}; + if (!add_overhead(cc.bytes_written, osi::TpktSpanCodec::header_length, required)) { + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + if (response.size() < required) { + return make_response_capacity(state_, required); + } + const auto encoded = osi::TpktSpanCodec::encode_into( + workspace.first(cc.bytes_written), response.first(required)); + if (!encoded.success()) { + if (encoded.status == wire::EncodeStatus::buffer_too_small) { + return make_response_capacity(state_, encoded.required_bytes); + } + state_ = MmsStaticConnectionState::fault; + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + negotiated_tpdu_size_bytes_ = selected_tpdu_bytes; + state_ = MmsStaticConnectionState::awaiting_association; + return make_result( + MmsStaticConnectionStatus::response_ready, + state_, + peek.frame_bytes, + encoded.bytes_written); + } + + if (cotp.kind != osi::CotpWireKind::data) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + bool using_reassembly = false; + std::span reassembly = policy_.cotp_reassembly; + const auto select_workspace_reassembly = + [this, workspace_storage, &workspace, &reassembly]() noexcept { + if (workspace_storage.size() < 2U) { + return false; + } + const auto processing_bytes = workspace_storage.size() / 2U; + const auto reassembly_bytes = workspace_storage.size() - processing_bytes; + if (processing_bytes == 0U || reassembly_bytes == 0U) { + return false; + } + if (cotp_reassembly_uses_workspace_) { + if (cotp_reassembly_workspace_data_ != workspace_storage.data() || + cotp_reassembly_workspace_size_ != workspace_storage.size()) { + return false; + } + } else { + cotp_reassembly_uses_workspace_ = true; + cotp_reassembly_workspace_data_ = workspace_storage.data(); + cotp_reassembly_workspace_size_ = workspace_storage.size(); + } + workspace = workspace_storage.first(processing_bytes); + reassembly = workspace_storage.subspan(processing_bytes, reassembly_bytes); + return true; + }; + + if (cotp_reassembly_complete_) { + if (cotp_reassembly_size_ == 0U || !cotp.end_of_transmission) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + if (cotp_reassembly_uses_workspace_ && !select_workspace_reassembly()) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + if (reassembly.empty() || cotp_reassembly_size_ > reassembly.size()) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + cotp.user_data = std::span{reassembly}.first(cotp_reassembly_size_); + using_reassembly = true; + } else if (cotp_reassembly_size_ != 0U || !cotp.end_of_transmission) { + if (reassembly.empty()) { + if (!policy_.require_end_of_transmission) { + // Explicit compatibility opt-out: expose a partial TPDU to the + // upper decoder exactly as the historical strict runtime did. + } else if (!select_workspace_reassembly()) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + } + if (!reassembly.empty()) { + if (cotp_reassembly_size_ > reassembly.size() || + cotp.user_data.size() > reassembly.size() - cotp_reassembly_size_) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + std::copy( + cotp.user_data.begin(), + cotp.user_data.end(), + reassembly.begin() + static_cast(cotp_reassembly_size_)); + cotp_reassembly_size_ += cotp.user_data.size(); + if (!cotp.end_of_transmission) { + return make_result( + MmsStaticConnectionStatus::consumed_no_response, + state_, + peek.frame_bytes); + } + cotp_reassembly_complete_ = true; + cotp.user_data = std::span{reassembly}.first(cotp_reassembly_size_); + using_reassembly = true; + } + } + + const auto finish = [this, using_reassembly]( + MmsStaticConnectionResult result) noexcept { + if (using_reassembly && result.consumed_bytes != 0U) { + clear_cotp_reassembly(); + } + return result; + }; + + if (state_ == MmsStaticConnectionState::awaiting_association) { + acse::AssociationRequestView association; + if (!acse::AcseSpanCodec::try_decode_association_request_compat_view( + cotp.user_data, association)) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + MmsInitiateView initiate; + if (!MmsPduSpanCodec::try_decode_initiate_request_view( + association.aarq.user_information.single_asn1_type, initiate) || + association.mms_presentation_context_id == 0U) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + const auto selected_mms_pdu_size = std::min( + initiate.maximum_mms_pdu_size, kServerMaximumMmsPduSize); + + const auto selected_outstanding_calling = std::min( + initiate.maximum_outstanding_calling, + kServerMaximumOutstandingCalling); + const auto selected_outstanding_called = std::min( + initiate.maximum_outstanding_called, + kServerMaximumOutstandingCalled); + const auto selected_nesting_level = std::min( + initiate.data_structure_nesting_level, + kServerMaximumNestingLevel); + std::array initiate_response{}; + const auto initiate_encoded = MmsPduSpanCodec::encode_initiate_response_into( + selected_mms_pdu_size, + selected_outstanding_calling, + selected_outstanding_called, + selected_nesting_level, + initiate_response); + if (!initiate_encoded.success()) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + const auto accept = acse::AcseSpanCodec::build_accept_response_into( + association, + std::span{initiate_response}.first( + initiate_encoded.bytes_written), + workspace); + if (!accept.success()) { + if (accept.status == wire::EncodeStatus::buffer_too_small) { + return make_workspace_capacity(state_, accept.required_bytes); + } + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + const auto wrapped = wrap_cotp_data_response( + workspace.first(accept.bytes_written), + peek.frame_bytes, + MmsStaticConnectionState::established, + negotiated_tpdu_size_bytes_, + response); + if (!wrapped.response_ready()) { + return wrapped; + } + mms_presentation_context_id_ = association.mms_presentation_context_id; + negotiated_mms_pdu_size_ = selected_mms_pdu_size; + state_ = MmsStaticConnectionState::established; + association_active_ = true; + association_close_notified_ = false; + auto result = wrapped; + result.state = state_; + return finish(result); + } + + if (state_ != MmsStaticConnectionState::established) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + osi::SessionDataTransferView session; + osi::PresentationPdvView pdv; + if (!osi::SessionSpanCodec::try_decode_data_transfer_view(cotp.user_data, session) || + !osi::PresentationSpanCodec::try_decode_fully_encoded_data_view( + session.presentation_payload, pdv) || + pdv.context_id != mms_presentation_context_id_) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result( + MmsStaticConnectionStatus::protocol_violation, + state_, + peek.frame_bytes); + } + + if (MmsPduSpanCodec::is_conclude_request(pdv.single_asn1_type)) { + const auto conclude = MmsPduSpanCodec::encode_conclude_response_into(response); + return finish(wrap_mms_response( + conclude, + peek.frame_bytes, + state_, + mms_presentation_context_id_, + negotiated_mms_pdu_size_, + negotiated_tpdu_size_bytes_, + response, + workspace, + MmsStaticDispatchStatus::response_ready, + MmsWireConfirmedService::unknown, + 0U)); + } + + MmsConfirmedPduView confirmed; + const bool confirmed_decoded = MmsPduSpanCodec::try_decode_confirmed_request_view( + pdv.single_asn1_type, confirmed); + if (confirmed_decoded) { + if (pdv.single_asn1_type.size() > negotiated_mms_pdu_size_) { + return finish(make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes)); + } + + if (confirmed.service() == MmsWireConfirmedService::identify) { + const auto identify = !confirmed.service_constructed && confirmed.service_value.empty() + ? MmsPduSpanCodec::encode_identify_response_into( + confirmed.invoke_id, response) + : MmsPduSpanCodec::encode_confirmed_error_into( + confirmed.invoke_id, response); + return finish(wrap_mms_response( + identify, + peek.frame_bytes, + state_, + mms_presentation_context_id_, + negotiated_mms_pdu_size_, + negotiated_tpdu_size_bytes_, + response, + workspace, + MmsStaticDispatchStatus::response_ready, + MmsWireConfirmedService::identify, + confirmed.invoke_id)); + } + + if (confirmed.service() == MmsWireConfirmedService::file_directory) { + const auto directory = confirmed.service_constructed + ? MmsPduSpanCodec::encode_confirmed_response_into( + confirmed.invoke_id, + static_cast(MmsWireConfirmedService::file_directory), + true, + kEmptyFileDirectoryFields, + response) + : MmsPduSpanCodec::encode_confirmed_error_into( + confirmed.invoke_id, response); + return finish(wrap_mms_response( + directory, + peek.frame_bytes, + state_, + mms_presentation_context_id_, + negotiated_mms_pdu_size_, + negotiated_tpdu_size_bytes_, + response, + workspace, + MmsStaticDispatchStatus::response_ready, + MmsWireConfirmedService::file_directory, + confirmed.invoke_id)); + } + + std::size_t write_mms_required{}; + std::size_t write_frame_required{}; + if (write_outer_capacity( + confirmed, + dispatcher_, + mms_presentation_context_id_, + negotiated_tpdu_size_bytes_, + write_mms_required, + write_frame_required)) { + if (negotiated_mms_pdu_size_ == 0U || + write_mms_required > negotiated_mms_pdu_size_) { + return finish(make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes)); + } + if (response.size() < write_frame_required) { + return make_response_capacity(state_, write_frame_required); + } + if (workspace.size() < write_frame_required) { + return make_workspace_capacity(state_, write_frame_required); + } + } + } + + const auto application = dispatcher_.dispatch( + pdv.single_asn1_type, response, workspace, policy_.access_context()); + if (!application.success()) { + if (application.status == MmsStaticDispatchStatus::response_buffer_too_small) { + if (negotiated_mms_pdu_size_ == 0U || + application.required_bytes > negotiated_mms_pdu_size_) { + return finish(make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes)); + } + const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( + mms_presentation_context_id_, application.required_bytes); + if (fully_encoded) { + std::size_t maximum_user_data{}; + std::size_t segment_count{}; + std::size_t required{}; + if (cotp_stream_size( + *fully_encoded, + negotiated_tpdu_size_bytes_, + maximum_user_data, + segment_count, + required)) { + auto result = make_response_capacity(state_, required); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; + } + } + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + if (application.status == MmsStaticDispatchStatus::workspace_too_small) { + auto result = make_workspace_capacity(state_, application.required_bytes); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; + } + if (application.status == MmsStaticDispatchStatus::backend_failure || + application.status == MmsStaticDispatchStatus::invalid_object_table) { + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + auto result = make_result( + MmsStaticConnectionStatus::backend_failure, + state_, + peek.frame_bytes); + result.application_status = application.status; + result.application_service = application.service; + result.invoke_id = application.invoke_id; + return result; + } + + if (confirmed_decoded && + application.status == MmsStaticDispatchStatus::object_not_found && + confirmed.service() == MmsWireConfirmedService::get_variable_access_attributes) { + MmsVariableAccessAttributesRequestView attributes; + if (MmsServiceSpanCodec::try_decode_variable_access_attributes_request( + confirmed, attributes) && + attributes.name.kind == MmsObjectNameViewKind::vmd_specific && + !attributes.name.item.empty()) { + const auto fallback = + MmsServiceSpanCodec::encode_variable_access_attributes_response_into( + confirmed.invoke_id, + false, + kConservativeStructureType, + response); + return finish(wrap_mms_response( + fallback, + peek.frame_bytes, + state_, + mms_presentation_context_id_, + negotiated_mms_pdu_size_, + negotiated_tpdu_size_bytes_, + response, + workspace, + application.status, + confirmed.service(), + confirmed.invoke_id)); + } + } + + if (confirmed_decoded && compatibility_error_status(application.status)) { + const auto error = MmsPduSpanCodec::encode_confirmed_error_into( + confirmed.invoke_id, response); + return finish(wrap_mms_response( + error, + peek.frame_bytes, + state_, + mms_presentation_context_id_, + negotiated_mms_pdu_size_, + negotiated_tpdu_size_bytes_, + response, + workspace, + application.status, + confirmed.service(), + confirmed.invoke_id)); + } + return finish(application_rejected(state_, peek.frame_bytes, application)); + } + + if (negotiated_mms_pdu_size_ == 0U || + application.bytes_written > negotiated_mms_pdu_size_) { + return finish(make_result( + MmsStaticConnectionStatus::peer_limit_exceeded, + state_, + peek.frame_bytes)); + } + + const auto p_data = osi::PresentationSpanCodec::encode_p_data_into( + response.first(application.bytes_written), + workspace, + mms_presentation_context_id_, + true); + if (!p_data.success()) { + if (p_data.status == wire::EncodeStatus::buffer_too_small) { + return make_workspace_capacity(state_, p_data.required_bytes); + } + state_ = MmsStaticConnectionState::fault; + clear_cotp_reassembly(); + return make_result(MmsStaticConnectionStatus::backend_failure, state_); + } + + auto wrapped = wrap_cotp_data_response( + workspace.first(p_data.bytes_written), + peek.frame_bytes, + state_, + negotiated_tpdu_size_bytes_, + response); + if (wrapped.response_ready()) { + wrapped.application_status = application.status; + wrapped.application_service = application.service; + wrapped.invoke_id = application.invoke_id; + } + return finish(wrapped); +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/data_set_span.cpp b/src/mms/data_set_span.cpp new file mode 100644 index 000000000..4deb74bc9 --- /dev/null +++ b/src/mms/data_set_span.cpp @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/data_set_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool valid_identifier(const std::string_view value) noexcept { + if (value.empty() || value.size() > MmsServiceSpanCodec::maximum_identifier_bytes) { + return false; + } + for (const char ch : value) { + const auto byte = static_cast(ch); + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] std::size_t minimal_unsigned_size(std::uint32_t value) noexcept { + std::size_t bytes = 1U; + while (value > 0xFFU) { + ++bytes; + value >>= 8U; + } + return bytes; +} + +[[nodiscard]] bool needs_positive_prefix( + const std::uint32_t value, + const std::size_t minimal_bytes) noexcept { + const auto shift = static_cast((minimal_bytes - 1U) * 8U); + return ((value >> shift) & 0x80U) != 0U; +} + +[[nodiscard]] std::size_t positive_integer_size(const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + return minimal + (needs_positive_prefix(value, minimal) ? 1U : 0U); +} + +[[nodiscard]] bool write_positive_integer( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + if (needs_positive_prefix(value, minimal) && !writer.write_byte(0x00U)) { + return false; + } + for (std::size_t index = minimal; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] std::optional checked_add( + const std::optional left, + const std::optional right) noexcept { + if (!left || !right || *right > std::numeric_limits::max() - *left) { + return std::nullopt; + } + return *left + *right; +} + +[[nodiscard]] std::optional member_encoded_size( + const MmsNamedVariableListMemberInput& member) noexcept { + if (!valid_identifier(member.domain) || !valid_identifier(member.item)) { + return std::nullopt; + } + const auto domain = asn1::BerSpanWriter::tlv_size(26, member.domain.size()); + const auto item = asn1::BerSpanWriter::tlv_size(26, member.item.size()); + const auto object_content = checked_add(domain, item); + if (!object_content) { + return std::nullopt; + } + const auto object_name = asn1::BerSpanWriter::tlv_size(1, *object_content); + if (!object_name) { + return std::nullopt; + } + const auto specification = asn1::BerSpanWriter::tlv_size(0, *object_name); + if (!specification) { + return std::nullopt; + } + return asn1::BerSpanWriter::tlv_size(16, *specification); +} + +struct ResponseSizing final { + std::size_t invoke_value{}; + std::size_t list_content{}; + std::size_t service_content{}; + std::size_t outer_content{}; + std::size_t required{}; +}; + +[[nodiscard]] std::optional response_sizing( + const std::uint32_t invoke_id, + const std::span members) noexcept { + if (invoke_id > MmsPduSpanCodec::maximum_invoke_id || + members.size() > MmsDataSetSpanCodec::maximum_members) { + return std::nullopt; + } + + std::size_t list_content = 0U; + for (const auto& member : members) { + const auto encoded = member_encoded_size(member); + if (!encoded || *encoded > std::numeric_limits::max() - list_content) { + return std::nullopt; + } + list_content += *encoded; + } + + const auto list_tlv = asn1::BerSpanWriter::tlv_size(1, list_content); + const auto deletable_tlv = asn1::BerSpanWriter::tlv_size(0, 1U); + const auto service_content = checked_add(deletable_tlv, list_tlv); + if (!list_tlv || !deletable_tlv || !service_content) { + return std::nullopt; + } + + const auto invoke_value = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(2, invoke_value); + const auto service_tlv = asn1::BerSpanWriter::tlv_size(12, *service_content); + const auto outer_content = checked_add(invoke_tlv, service_tlv); + if (!invoke_tlv || !service_tlv || !outer_content) { + return std::nullopt; + } + const auto required = asn1::BerSpanWriter::tlv_size(1, *outer_content); + if (!required || *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return std::nullopt; + } + + return ResponseSizing{ + invoke_value, + list_content, + *service_content, + *outer_content, + *required}; +} + +[[nodiscard]] bool decode_member_definition( + const asn1::BerTlvView& definition, + MmsObjectNameView& name) noexcept { + name = {}; + if (definition.tag_class != asn1::BerClass::universal || + definition.tag_number != 16 || !definition.constructed) { + return false; + } + asn1::BerTlvView specification; + if (!asn1::BerSpanReader::try_read_exact(definition.value, specification) || + specification.tag_class != asn1::BerClass::context_specific || + specification.tag_number != 0 || !specification.constructed) { + return false; + } + if (!MmsServiceSpanCodec::try_decode_object_name_view(specification.value, name)) { + return false; + } + return name.kind == MmsObjectNameViewKind::domain_specific && + !name.domain.empty() && !name.item.empty(); +} + +[[nodiscard]] bool validate_member_list( + const std::span bytes, + std::size_t& count) noexcept { + count = 0U; + std::size_t offset = 0U; + while (offset < bytes.size()) { + if (count >= MmsDataSetSpanCodec::maximum_members) { + count = 0U; + return false; + } + asn1::BerTlvView definition; + MmsObjectNameView name; + if (!asn1::BerSpanReader::try_read_tlv(bytes, offset, definition) || + !decode_member_definition(definition, name)) { + count = 0U; + return false; + } + ++count; + } + return true; +} + +} // namespace + +bool MmsNamedVariableListAttributesResponseView::try_member( + const std::size_t index, + MmsObjectNameView& name) const noexcept { + name = {}; + if (index >= member_count) { + return false; + } + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < variable_list.size()) { + asn1::BerTlvView definition; + if (!asn1::BerSpanReader::try_read_tlv(variable_list, offset, definition)) { + return false; + } + if (current == index) { + return decode_member_definition(definition, name); + } + ++current; + } + return false; +} + +bool MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_request( + const MmsConfirmedPduView& confirmed, + MmsNamedVariableListAttributesRequestView& request) noexcept { + request = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_request || + confirmed.service_tag != 12 || !confirmed.service_constructed) { + return false; + } + MmsObjectNameView name; + if (!MmsServiceSpanCodec::try_decode_object_name_view(confirmed.service_value, name) || + name.kind != MmsObjectNameViewKind::domain_specific) { + return false; + } + request.invoke_id = confirmed.invoke_id; + request.name = name; + return true; +} + +bool MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_request( + const std::span mms_pdu, + MmsNamedVariableListAttributesRequestView& request) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_request_view(mms_pdu, confirmed) && + try_decode_get_named_variable_list_attributes_request(confirmed, request); +} + +bool MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_response( + const MmsConfirmedPduView& confirmed, + MmsNamedVariableListAttributesResponseView& response) noexcept { + response = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_response || + confirmed.service_tag != 12 || !confirmed.service_constructed) { + return false; + } + + std::size_t offset = 0U; + asn1::BerTlvView deletable; + asn1::BerTlvView list; + if (!asn1::BerSpanReader::try_read_tlv(confirmed.service_value, offset, deletable) || + !asn1::BerSpanReader::try_read_tlv(confirmed.service_value, offset, list) || + offset != confirmed.service_value.size() || + deletable.tag_class != asn1::BerClass::context_specific || + deletable.tag_number != 0 || deletable.constructed || deletable.value.size() != 1U || + list.tag_class != asn1::BerClass::context_specific || + list.tag_number != 1 || !list.constructed) { + return false; + } + + std::size_t member_count = 0U; + if (!validate_member_list(list.value, member_count)) { + return false; + } + response.invoke_id = confirmed.invoke_id; + response.mms_deletable = deletable.value.front() != 0U; + response.variable_list = list.value; + response.member_count = member_count; + return true; +} + +bool MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_response( + const std::span mms_pdu, + MmsNamedVariableListAttributesResponseView& response) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_response_view(mms_pdu, confirmed) && + try_decode_get_named_variable_list_attributes_response(confirmed, response); +} + +wire::EncodeResult MmsDataSetSpanCodec::encode_get_named_variable_list_attributes_response_into( + const std::uint32_t invoke_id, + const bool mms_deletable, + const std::span members, + const std::span destination) noexcept { + const auto sizing = response_sizing(invoke_id, members); + if (!sizing) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < sizing->required) { + return {wire::EncodeStatus::buffer_too_small, 0U, sizing->required}; + } + + asn1::BerSpanWriter writer{destination.first(sizing->required)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, sizing->outer_content) || + !writer.write_tlv_header( + asn1::BerClass::universal, false, 2, sizing->invoke_value) || + !write_positive_integer(writer, invoke_id) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 12, sizing->service_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, 1U) || + !writer.write_byte(mms_deletable ? 0xFFU : 0x00U) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, sizing->list_content)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + + for (const auto& member : members) { + const auto domain_tlv = asn1::BerSpanWriter::tlv_size(26, member.domain.size()); + const auto item_tlv = asn1::BerSpanWriter::tlv_size(26, member.item.size()); + const auto object_content = checked_add(domain_tlv, item_tlv); + const auto object_name = object_content + ? asn1::BerSpanWriter::tlv_size(1, *object_content) + : std::nullopt; + const auto specification = object_name + ? asn1::BerSpanWriter::tlv_size(0, *object_name) + : std::nullopt; + if (!domain_tlv || !item_tlv || !object_content || !object_name || !specification || + !writer.write_tlv_header( + asn1::BerClass::universal, true, 16, *specification) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 0, *object_name) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, *object_content) || + !writer.write_tlv( + asn1::BerClass::universal, + false, + 26, + std::span{ + reinterpret_cast(member.domain.data()), + member.domain.size()}) || + !writer.write_tlv( + asn1::BerClass::universal, + false, + 26, + std::span{ + reinterpret_cast(member.item.data()), + member.item.size()})) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + } + + if (writer.size() != sizing->required) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + return {wire::EncodeStatus::ok, sizing->required, sizing->required}; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/information_report_span.cpp b/src/mms/information_report_span.cpp new file mode 100644 index 000000000..85afbf644 --- /dev/null +++ b/src/mms/information_report_span.cpp @@ -0,0 +1,606 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/information_report_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" +#include "ariec61850/mms/pdu_span.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::uint8_t kOptSequenceNumber = 0x40U; +constexpr std::uint8_t kOptReportTimeStamp = 0x20U; +constexpr std::uint8_t kOptReasonForInclusion = 0x10U; +constexpr std::uint8_t kOptDataSetName = 0x08U; +constexpr std::uint8_t kOptDataReference = 0x04U; +constexpr std::uint8_t kOptBufferOverflow = 0x02U; +constexpr std::uint8_t kOptEntryId = 0x01U; +constexpr std::uint8_t kOptConfRevision = 0x80U; +constexpr std::uint8_t kOptSegmentation = 0x40U; + +[[nodiscard]] bool visible_ascii( + const std::string_view text, + const std::size_t maximum_bytes) noexcept { + if (text.empty() || text.size() > maximum_bytes) { + return false; + } + for (const auto character : text) { + const auto byte = static_cast(character); + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] bool span_equals( + const std::span bytes, + const std::string_view text) noexcept { + if (bytes.size() != text.size()) { + return false; + } + for (std::size_t index = 0U; index < bytes.size(); ++index) { + if (bytes[index] != static_cast( + static_cast(text[index]))) { + return false; + } + } + return true; +} + +[[nodiscard]] std::optional add_size( + const std::optional left, + const std::optional right) noexcept { + if (!left || !right || *right > std::numeric_limits::max() - *left) { + return std::nullopt; + } + return *left + *right; +} + +[[nodiscard]] bool add_to( + std::size_t& total, + const std::optional value) noexcept { + if (!value || *value > std::numeric_limits::max() - total) { + return false; + } + total += *value; + return true; +} + +[[nodiscard]] std::size_t minimal_unsigned_size(std::uint32_t value) noexcept { + std::size_t bytes = 1U; + while (value > 0xFFU) { + ++bytes; + value >>= 8U; + } + return bytes; +} + +[[nodiscard]] bool needs_positive_prefix( + const std::uint32_t value, + const std::size_t minimal_bytes) noexcept { + const auto shift = static_cast((minimal_bytes - 1U) * 8U); + return ((value >> shift) & 0x80U) != 0U; +} + +[[nodiscard]] std::size_t positive_integer_size(const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + return minimal + (needs_positive_prefix(value, minimal) ? 1U : 0U); +} + +[[nodiscard]] bool write_unsigned_content( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto bytes = minimal_unsigned_size(value); + for (std::size_t index = bytes; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] bool write_positive_content( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto bytes = minimal_unsigned_size(value); + if (needs_positive_prefix(value, bytes) && !writer.write_byte(0x00U)) { + return false; + } + return write_unsigned_content(writer, value); +} + +[[nodiscard]] bool valid_mms_data_tlv(const asn1::BerTlvView& tlv) noexcept { + if (tlv.tag_class != asn1::BerClass::context_specific) { + return false; + } + if (tlv.tag_number == 1 || tlv.tag_number == 2) { + return tlv.constructed; + } + return tlv.tag_number >= 3 && tlv.tag_number <= 17 && !tlv.constructed; +} + +[[nodiscard]] bool decode_access_result( + const std::span encoded, + MmsReadAccessResultView& result) noexcept { + result = {}; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv)) { + return false; + } + if (tlv.tag_class == asn1::BerClass::context_specific && + tlv.tag_number == 0 && !tlv.constructed) { + const auto failure = asn1::BerSpanReader::read_uint32(tlv); + if (!failure) { + return false; + } + result.success = false; + result.failure_code = *failure; + return true; + } + if (!valid_mms_data_tlv(tlv)) { + return false; + } + result.success = true; + result.encoded_data = encoded; + return true; +} + +[[nodiscard]] std::optional access_result_size( + const MmsReadAccessResultInput& result) noexcept { + if (result.success) { + asn1::BerTlvView data; + if (!asn1::BerSpanReader::try_read_exact(result.encoded_data, data) || + !valid_mms_data_tlv(data)) { + return std::nullopt; + } + return result.encoded_data.size(); + } + return asn1::BerSpanWriter::tlv_size(0, positive_integer_size(result.failure_code)); +} + +[[nodiscard]] bool write_access_result( + asn1::BerSpanWriter& writer, + const MmsReadAccessResultInput& result) noexcept { + if (result.success) { + return writer.write_bytes(result.encoded_data); + } + const auto content = positive_integer_size(result.failure_code); + return writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, content) && + write_positive_content(writer, result.failure_code); +} + +[[nodiscard]] std::optional reference_content_size( + const MmsInformationReportReferenceInput& reference) noexcept { + if (!visible_ascii(reference.domain, MmsServiceSpanCodec::maximum_identifier_bytes) || + !visible_ascii(reference.item, MmsServiceSpanCodec::maximum_identifier_bytes)) { + return std::nullopt; + } + if (reference.item.size() > + MmsInformationReportSpanCodec::maximum_reference_bytes - 1U || + reference.domain.size() > + MmsInformationReportSpanCodec::maximum_reference_bytes - 1U - reference.item.size()) { + return std::nullopt; + } + return reference.domain.size() + 1U + reference.item.size(); +} + +[[nodiscard]] bool write_reference_data( + asn1::BerSpanWriter& writer, + const MmsInformationReportReferenceInput& reference) noexcept { + const auto content = reference_content_size(reference); + if (!content || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, *content) || + !writer.write_bytes(std::span{ + reinterpret_cast(reference.domain.data()), + reference.domain.size()}) || + !writer.write_byte(static_cast('/')) || + !writer.write_bytes(std::span{ + reinterpret_cast(reference.item.data()), + reference.item.size()})) { + return false; + } + return true; +} + +[[nodiscard]] bool write_visible_data( + asn1::BerSpanWriter& writer, + const std::string_view text) noexcept { + return writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, text.size()) && + writer.write_bytes(std::span{ + reinterpret_cast(text.data()), text.size()}); +} + +[[nodiscard]] bool write_bit_string_data( + asn1::BerSpanWriter& writer, + const std::uint8_t unused_bits, + const std::span bytes) noexcept { + return unused_bits <= 7U && + writer.write_tlv_header( + asn1::BerClass::context_specific, false, 4, 1U + bytes.size()) && + writer.write_byte(unused_bits) && writer.write_bytes(bytes); +} + +[[nodiscard]] bool write_unsigned_data( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto content = minimal_unsigned_size(value); + return writer.write_tlv_header( + asn1::BerClass::context_specific, false, 6, content) && + write_unsigned_content(writer, value); +} + +[[nodiscard]] bool write_boolean_data( + asn1::BerSpanWriter& writer, + const bool value) noexcept { + return writer.write_tlv_header( + asn1::BerClass::context_specific, false, 3, 1U) && + writer.write_byte(value ? 0x01U : 0x00U); +} + +[[nodiscard]] bool optional_selected( + const MmsInformationReportSnapshotInput& report, + const std::size_t byte_index, + const std::uint8_t mask) noexcept { + return report.optional_fields.size() > byte_index && + (report.optional_fields[byte_index] & mask) != 0U; +} + +[[nodiscard]] bool validate_report( + const MmsInformationReportSnapshotInput& report) noexcept { + if (!visible_ascii( + report.report_id, + MmsInformationReportSpanCodec::maximum_report_id_bytes) || + report.optional_fields.size() != + MmsInformationReportSpanCodec::optional_field_bytes || + report.member_results.empty() || + report.member_results.size() > MmsInformationReportSpanCodec::maximum_members || + report.member_references.size() != report.member_results.size()) { + return false; + } + if ((report.optional_fields[0] & 0x80U) != 0U || + (report.optional_fields[1] & kOptSegmentation) != 0U || + (report.optional_fields[1] & 0x3FU) != 0U) { + return false; + } + if (!reference_content_size(report.data_set_reference)) { + return false; + } + for (const auto& reference : report.member_references) { + if (!reference_content_size(reference)) { + return false; + } + } + if (optional_selected(report, 0U, kOptReportTimeStamp) && + report.report_time.size() != MmsInformationReportSpanCodec::binary_time_bytes) { + return false; + } + if ((optional_selected(report, 0U, kOptBufferOverflow) || + optional_selected(report, 0U, kOptEntryId)) && !report.buffered) { + return false; + } + if (optional_selected(report, 0U, kOptEntryId) && + report.entry_id.size() != MmsInformationReportSpanCodec::entry_id_bytes) { + return false; + } + if (optional_selected(report, 0U, kOptReasonForInclusion) && + (report.reason_for_inclusion & 0x03U) != 0U) { + return false; + } + for (const auto& result : report.member_results) { + if (!access_result_size(result)) { + return false; + } + } + return true; +} + +[[nodiscard]] std::optional access_list_content_size( + const MmsInformationReportSnapshotInput& report) noexcept { + std::size_t total = 0U; + if (!add_to(total, asn1::BerSpanWriter::tlv_size(10, report.report_id.size())) || + !add_to(total, asn1::BerSpanWriter::tlv_size( + 4, 1U + report.optional_fields.size()))) { + return std::nullopt; + } + + if (optional_selected(report, 0U, kOptSequenceNumber) && + !add_to(total, asn1::BerSpanWriter::tlv_size( + 6, minimal_unsigned_size(report.sequence_number)))) { + return std::nullopt; + } + if (optional_selected(report, 0U, kOptReportTimeStamp) && + !add_to(total, asn1::BerSpanWriter::tlv_size(12, report.report_time.size()))) { + return std::nullopt; + } + if (optional_selected(report, 0U, kOptDataSetName)) { + const auto content = reference_content_size(report.data_set_reference); + if (!content || !add_to(total, asn1::BerSpanWriter::tlv_size(10, *content))) { + return std::nullopt; + } + } + if (optional_selected(report, 0U, kOptBufferOverflow) && + !add_to(total, asn1::BerSpanWriter::tlv_size(3, 1U))) { + return std::nullopt; + } + if (optional_selected(report, 0U, kOptEntryId) && + !add_to(total, asn1::BerSpanWriter::tlv_size(9, report.entry_id.size()))) { + return std::nullopt; + } + if (optional_selected(report, 1U, kOptConfRevision) && + !add_to(total, asn1::BerSpanWriter::tlv_size( + 6, minimal_unsigned_size(report.conf_revision)))) { + return std::nullopt; + } + + const auto inclusion_bytes = (report.member_results.size() + 7U) / 8U; + if (!add_to(total, asn1::BerSpanWriter::tlv_size(4, 1U + inclusion_bytes))) { + return std::nullopt; + } + + if (optional_selected(report, 0U, kOptDataReference)) { + for (const auto& reference : report.member_references) { + const auto content = reference_content_size(reference); + if (!content || !add_to(total, asn1::BerSpanWriter::tlv_size(10, *content))) { + return std::nullopt; + } + } + } + + for (const auto& result : report.member_results) { + if (!add_to(total, access_result_size(result))) { + return std::nullopt; + } + } + + if (optional_selected(report, 0U, kOptReasonForInclusion)) { + const auto reason_tlv = asn1::BerSpanWriter::tlv_size(4, 2U); + for (std::size_t index = 0U; index < report.member_results.size(); ++index) { + if (!add_to(total, reason_tlv)) { + return std::nullopt; + } + } + } + return total; +} + +[[nodiscard]] bool write_inclusion( + asn1::BerSpanWriter& writer, + const std::size_t member_count) noexcept { + const auto bytes = (member_count + 7U) / 8U; + const auto unused = static_cast((bytes * 8U) - member_count); + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 4, 1U + bytes) || + !writer.write_byte(unused)) { + return false; + } + for (std::size_t index = 0U; index < bytes; ++index) { + const auto first_member = index * 8U; + const auto remaining = member_count - first_member; + const auto used = remaining >= 8U ? 8U : remaining; + const auto value = used == 8U + ? std::uint8_t{0xFFU} + : static_cast(0xFFU << (8U - used)); + if (!writer.write_byte(value)) { + return false; + } + } + return true; +} + +} // namespace + +bool MmsInformationReportView::try_item( + const std::size_t index, + MmsReadAccessResultView& result) const noexcept { + result = {}; + if (index >= item_count) { + return false; + } + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < access_result_list.size()) { + const auto start = offset; + asn1::BerTlvView item; + if (!asn1::BerSpanReader::try_read_tlv(access_result_list, offset, item)) { + return false; + } + if (current == index) { + return decode_access_result( + access_result_list.subspan(start, offset - start), result); + } + ++current; + } + return false; +} + +bool MmsInformationReportSpanCodec::try_decode_information_report( + const std::span mms_pdu, + MmsInformationReportView& report) noexcept { + report = {}; + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(mms_pdu, outer) || + outer.tag_class != asn1::BerClass::context_specific || + outer.tag_number != 3 || !outer.constructed) { + return false; + } + + asn1::BerTlvView service; + if (!asn1::BerSpanReader::try_read_exact(outer.value, service) || + service.tag_class != asn1::BerClass::context_specific || + service.tag_number != 0 || !service.constructed) { + return false; + } + + std::size_t offset = 0U; + asn1::BerTlvView variable_access; + asn1::BerTlvView access_results; + if (!asn1::BerSpanReader::try_read_tlv(service.value, offset, variable_access) || + !asn1::BerSpanReader::try_read_tlv(service.value, offset, access_results) || + offset != service.value.size() || + variable_access.tag_class != asn1::BerClass::context_specific || + variable_access.tag_number != 1 || !variable_access.constructed || + access_results.tag_class != asn1::BerClass::context_specific || + access_results.tag_number != 0 || !access_results.constructed) { + return false; + } + + MmsObjectNameView list_name; + if (!MmsServiceSpanCodec::try_decode_object_name_view( + variable_access.value, list_name) || + list_name.kind != MmsObjectNameViewKind::vmd_specific || + !span_equals(list_name.item, "RPT")) { + return false; + } + + std::size_t result_offset = 0U; + std::size_t count = 0U; + while (result_offset < access_results.value.size()) { + if (count >= maximum_access_results) { + return false; + } + const auto start = result_offset; + asn1::BerTlvView item; + if (!asn1::BerSpanReader::try_read_tlv( + access_results.value, result_offset, item)) { + return false; + } + MmsReadAccessResultView decoded; + if (!decode_access_result( + access_results.value.subspan(start, result_offset - start), decoded)) { + return false; + } + ++count; + } + if (count == 0U) { + return false; + } + + report.variable_list_name = list_name; + report.access_result_list = access_results.value; + report.item_count = count; + return true; +} + +wire::EncodeResult MmsInformationReportSpanCodec::encode_snapshot_into( + const MmsInformationReportSnapshotInput& report, + const std::span destination) noexcept { + if (!validate_report(report)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto access_content = access_list_content_size(report); + const auto access_tlv = access_content + ? asn1::BerSpanWriter::tlv_size(0, *access_content) + : std::nullopt; + const auto vmd_name_tlv = asn1::BerSpanWriter::tlv_size(0, 3U); + const auto variable_access_tlv = vmd_name_tlv + ? asn1::BerSpanWriter::tlv_size(1, *vmd_name_tlv) + : std::nullopt; + const auto information_content = add_size(variable_access_tlv, access_tlv); + const auto service_tlv = information_content + ? asn1::BerSpanWriter::tlv_size(0, *information_content) + : std::nullopt; + const auto required = service_tlv + ? asn1::BerSpanWriter::tlv_size(3, *service_tlv) + : std::nullopt; + if (!access_content || !access_tlv || !vmd_name_tlv || + !variable_access_tlv || !information_content || !service_tlv || !required || + *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, true, 3, *service_tlv) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 0, *information_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, *vmd_name_tlv) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, 3U) || + !writer.write_bytes(std::span{ + reinterpret_cast("RPT"), 3U}) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 0, *access_content) || + !write_visible_data(writer, report.report_id) || + !write_bit_string_data(writer, 6U, report.optional_fields)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + + if (optional_selected(report, 0U, kOptSequenceNumber) && + !write_unsigned_data(writer, report.sequence_number)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, kOptReportTimeStamp) && + (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 12, report.report_time.size()) || + !writer.write_bytes(report.report_time))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, kOptDataSetName) && + !write_reference_data(writer, report.data_set_reference)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, kOptBufferOverflow) && + !write_boolean_data(writer, report.buffer_overflow)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 0U, kOptEntryId) && + (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 9, report.entry_id.size()) || + !writer.write_bytes(report.entry_id))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (optional_selected(report, 1U, kOptConfRevision) && + !write_unsigned_data(writer, report.conf_revision)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + if (!write_inclusion(writer, report.member_results.size())) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + + if (optional_selected(report, 0U, kOptDataReference)) { + for (const auto& reference : report.member_references) { + if (!write_reference_data(writer, reference)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + } + for (const auto& result : report.member_results) { + if (!write_access_result(writer, result)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + if (optional_selected(report, 0U, kOptReasonForInclusion)) { + const std::uint8_t reason = report.reason_for_inclusion; + for (std::size_t index = 0U; index < report.member_results.size(); ++index) { + if (!write_bit_string_data( + writer, 2U, std::span{&reason, 1U})) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + } + + if (writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/pdu_compat_span.cpp b/src/mms/pdu_compat_span.cpp new file mode 100644 index 000000000..1a6a1cf0d --- /dev/null +++ b/src/mms/pdu_compat_span.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/pdu_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::array kIdentifyFields{ + 0x80U, 0x0AU, + 'A','R','I','E','C','6','1','8','5','0', + 0x81U, 0x15U, + 'V','i','r','t','u','a','l',' ','I','E','D',' ','S','i','m','u','l','a','t','o','r', + 0x82U, 0x03U, '1','.','0'}; + +[[nodiscard]] std::size_t positive_integer_size(const std::uint32_t value) noexcept { + std::size_t bytes = 1U; + auto remaining = value; + while (remaining > 0xFFU) { + ++bytes; + remaining >>= 8U; + } + const auto leading = static_cast( + value >> static_cast((bytes - 1U) * 8U)); + return bytes + ((leading & 0x80U) != 0U ? 1U : 0U); +} + +[[nodiscard]] bool write_positive_integer( + asn1::BerSpanWriter& writer, + const std::uint32_t value, + const std::size_t bytes) noexcept { + const auto encoded_bytes = bytes - + ((bytes > 1U && (value >> static_cast((bytes - 2U) * 8U)) <= 0xFFU) + ? 1U + : 0U); + if (encoded_bytes < bytes && !writer.write_byte(0U)) { + return false; + } + for (std::size_t index = encoded_bytes; index > 0U; --index) { + const auto shift = static_cast((index - 1U) * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +} // namespace + +wire::EncodeResult MmsPduSpanCodec::encode_identify_response_into( + const std::uint32_t invoke_id, + const std::span destination) noexcept { + return encode_confirmed_response_into( + invoke_id, + static_cast(MmsWireConfirmedService::identify), + true, + kIdentifyFields, + destination); +} + +wire::EncodeResult MmsPduSpanCodec::encode_confirmed_error_into( + const std::uint32_t invoke_id, + const std::span destination) noexcept { + if (invoke_id > maximum_invoke_id) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + // ConfirmedErrorPDU ::= [2] { invokeID [0], serviceError [2] } + // serviceError.errorClass uses access[7] object-non-existent(2). This is + // the interoperable outcome for optional IEC 61850 objects such as Cancel: + // the request was syntactically valid, but the named service object is not + // present. A RejectPDU would incorrectly classify the request itself as + // malformed and abort otherwise valid control discovery clients. + const auto invoke_bytes = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(0, invoke_bytes); + const auto access_tlv = asn1::BerSpanWriter::tlv_size(7, 1U); + if (!invoke_tlv || !access_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto error_class_tlv = asn1::BerSpanWriter::tlv_size(0, *access_tlv); + if (!error_class_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto service_error_tlv = asn1::BerSpanWriter::tlv_size(2, *error_class_tlv); + if (!service_error_tlv || + *invoke_tlv > std::numeric_limits::max() - *service_error_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content = *invoke_tlv + *service_error_tlv; + const auto required = asn1::BerSpanWriter::tlv_size(2, content); + if (!required) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header(asn1::BerClass::context_specific, true, 2, content) || + !writer.write_tlv_header(asn1::BerClass::context_specific, false, 0, invoke_bytes) || + !write_positive_integer(writer, invoke_id, invoke_bytes) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 2, *error_class_tlv) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 0, *access_tlv) || + !writer.write_tlv_header(asn1::BerClass::context_specific, false, 7, 1U) || + !writer.write_byte(2U) || writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +bool MmsPduSpanCodec::is_conclude_request( + const std::span bytes) noexcept { + asn1::BerTlvView pdu; + return asn1::BerSpanReader::try_read_exact(bytes, pdu) && + pdu.tag_class == asn1::BerClass::context_specific && + pdu.tag_number == 11 && !pdu.constructed && pdu.value.empty(); +} + +wire::EncodeResult MmsPduSpanCodec::encode_conclude_response_into( + const std::span destination) noexcept { + constexpr std::size_t required = 2U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x8CU; + destination[1] = 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/pdu_span.cpp b/src/mms/pdu_span.cpp new file mode 100644 index 000000000..e58584ba8 --- /dev/null +++ b/src/mms/pdu_span.cpp @@ -0,0 +1,539 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/pdu_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +// Advertise only the bounded static dispatcher capability surface: +// GetNameList(1), Read(4), Write(5), GetVariableAccessAttributes(6), and +// GetNamedVariableListAttributes(12). Do not echo the client's requested +// service mask because that would claim services the server cannot dispatch. +constexpr std::array kDefaultInitiateResponse{ + 0xA9U, 0x26U, + 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, + 0x81U, 0x01U, 0x0AU, + 0x82U, 0x01U, 0x0AU, + 0x83U, 0x01U, 0x05U, + 0xA4U, 0x16U, + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +constexpr std::array kInitiateResponseDetail{ + 0x80U, 0x01U, 0x01U, + 0x81U, 0x03U, 0x05U, 0xF1U, 0x00U, + 0x82U, 0x0CU, 0x03U, 0x4EU, 0x08U, 0x00U, 0x00U, 0x00U, + 0x00U, 0x00U, 0x00U, 0x00U, 0x00U, 0x00U}; + +[[nodiscard]] bool read_u32( + const asn1::BerTlvView& tlv, + std::uint32_t& value) noexcept { + const auto decoded = asn1::BerSpanReader::read_uint32(tlv); + if (!decoded) { + value = 0U; + return false; + } + value = *decoded; + return true; +} + +[[nodiscard]] bool decode_detail( + const asn1::BerTlvView& field, + MmsInitiateDetailView& detail) noexcept { + detail = {}; + if (field.tag_class != asn1::BerClass::context_specific || + field.tag_number != 4 || !field.constructed) { + return false; + } + + std::array seen{}; + std::size_t count = 0U; + std::size_t offset = 0U; + while (offset < field.value.size()) { + if (count >= 3U) { + detail = {}; + return false; + } + ++count; + asn1::BerTlvView child; + if (!asn1::BerSpanReader::try_read_tlv(field.value, offset, child) || + child.tag_class != asn1::BerClass::context_specific || + child.constructed || child.tag_number < 0 || child.tag_number > 2) { + detail = {}; + return false; + } + const auto index = static_cast(child.tag_number); + if (seen[index]) { + detail = {}; + return false; + } + seen[index] = true; + + if (index == 0U) { + if (!read_u32(child, detail.version_number)) { + detail = {}; + return false; + } + } else if (index == 1U) { + if (child.value.empty() || child.value.size() > + MmsPduSpanCodec::maximum_bit_string_bytes) { + detail = {}; + return false; + } + detail.parameter_support_options = child.value; + } else { + if (child.value.empty() || child.value.size() > + MmsPduSpanCodec::maximum_bit_string_bytes) { + detail = {}; + return false; + } + detail.services_supported_calling = child.value; + } + } + return count == 3U && seen[0] && seen[1] && seen[2]; +} + +[[nodiscard]] bool decode_initiate( + const std::span bytes, + const std::int32_t expected_tag, + const MmsWirePduKind kind, + MmsInitiateView& result) noexcept { + result = {}; + if (bytes.empty() || bytes.size() > MmsPduSpanCodec::maximum_pdu_bytes) { + return false; + } + + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(bytes, outer) || + outer.tag_class != asn1::BerClass::context_specific || + outer.tag_number != expected_tag || !outer.constructed) { + return false; + } + + std::array seen{}; + std::array values{}; + MmsInitiateDetailView detail; + std::size_t count = 0U; + std::size_t offset = 0U; + while (offset < outer.value.size()) { + if (count >= 5U) { + result = {}; + return false; + } + ++count; + asn1::BerTlvView child; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, child) || + child.tag_class != asn1::BerClass::context_specific || + child.tag_number < 0 || child.tag_number > 4) { + result = {}; + return false; + } + const auto index = static_cast(child.tag_number); + if (seen[index]) { + result = {}; + return false; + } + seen[index] = true; + + if (index < 4U) { + if (child.constructed || !read_u32(child, values[index])) { + result = {}; + return false; + } + } else if (!decode_detail(child, detail)) { + result = {}; + return false; + } + } + + // ISO 9506 InitiateRequest permits localDetailCalling [0] and + // proposedDataStructureNestingLevel [3] to be absent. The original + // ARIEC61850 server accepted such request profiles and several engineering + // clients rely on that tolerance. Keep InitiateResponse decoding strict, + // but do not turn standards-valid optional omissions into a protocol fault. + const bool is_request = kind == MmsWirePduKind::initiate_request; + const bool mandatory_present = seen[1] && seen[2] && seen[4]; + const bool response_complete = seen[0] && seen[1] && seen[2] && seen[3] && seen[4]; + if (!mandatory_present || (!is_request && !response_complete) || + (seen[0] && (values[0] < 64U || values[0] > MmsPduSpanCodec::maximum_pdu_bytes)) || + values[1] == 0U || values[2] == 0U || + (seen[3] && values[3] == 0U)) { + result = {}; + return false; + } + + result.kind = kind; + result.maximum_mms_pdu_size = seen[0] + ? values[0] + : static_cast(MmsPduSpanCodec::maximum_pdu_bytes); + result.maximum_outstanding_calling = values[1]; + result.maximum_outstanding_called = values[2]; + result.data_structure_nesting_level = seen[3] ? values[3] : 5U; + result.detail = detail; + return true; +} + +[[nodiscard]] bool decode_invoke( + const asn1::BerTlvView& tlv, + std::uint32_t& invoke_id) noexcept { + const bool universal_integer = + tlv.tag_class == asn1::BerClass::universal && + tlv.tag_number == 2 && !tlv.constructed; + const bool implicit_invoke = + tlv.tag_class == asn1::BerClass::context_specific && + tlv.tag_number == 0 && !tlv.constructed; + if ((!universal_integer && !implicit_invoke) || !read_u32(tlv, invoke_id) || + invoke_id > MmsPduSpanCodec::maximum_invoke_id) { + invoke_id = 0U; + return false; + } + return true; +} + +[[nodiscard]] bool decode_confirmed( + const std::span bytes, + const std::int32_t expected_outer_tag, + const MmsWirePduKind kind, + MmsConfirmedPduView& result) noexcept { + result = {}; + if (bytes.empty() || bytes.size() > MmsPduSpanCodec::maximum_pdu_bytes) { + return false; + } + + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(bytes, outer) || + outer.tag_class != asn1::BerClass::context_specific || + outer.tag_number != expected_outer_tag || !outer.constructed) { + return false; + } + + asn1::BerTlvView first; + asn1::BerTlvView last; + std::size_t count = 0U; + std::size_t offset = 0U; + while (offset < outer.value.size()) { + if (count >= 4U) { + result = {}; + return false; + } + asn1::BerTlvView child; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, child)) { + result = {}; + return false; + } + if (count == 0U) { + first = child; + } + last = child; + ++count; + } + + if (count < 2U || !decode_invoke(first, result.invoke_id) || + last.tag_class != asn1::BerClass::context_specific || + last.tag_number < 0) { + result = {}; + return false; + } + + result.kind = kind; + result.service_tag = last.tag_number; + result.service_constructed = last.constructed; + result.service_value = last.value; + return true; +} + +[[nodiscard]] std::size_t minimal_unsigned_size(std::uint32_t value) noexcept { + std::size_t bytes = 1U; + while (value > 0xFFU) { + ++bytes; + value >>= 8U; + } + return bytes; +} + +[[nodiscard]] bool needs_positive_prefix( + const std::uint32_t value, + const std::size_t minimal_bytes) noexcept { + const auto shift = static_cast((minimal_bytes - 1U) * 8U); + return ((value >> shift) & 0x80U) != 0U; +} + +[[nodiscard]] std::size_t positive_integer_size(const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + return minimal + (needs_positive_prefix(value, minimal) ? 1U : 0U); +} + +[[nodiscard]] bool write_positive_integer( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + if (needs_positive_prefix(value, minimal) && !writer.write_byte(0x00U)) { + return false; + } + for (std::size_t index = minimal; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast( + (value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] wire::EncodeResult encode_confirmed( + const std::int32_t outer_tag, + const std::uint32_t invoke_id, + const std::int32_t service_tag, + const bool service_constructed, + const std::span service_value, + const std::span destination) noexcept { + if (invoke_id > MmsPduSpanCodec::maximum_invoke_id || + service_tag < 0 || service_tag > 1'000'000 || + service_value.size() > MmsPduSpanCodec::maximum_pdu_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto invoke_value = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(2, invoke_value); + const auto service_tlv = asn1::BerSpanWriter::tlv_size( + service_tag, service_value.size()); + if (!invoke_tlv || !service_tlv || + *service_tlv > std::numeric_limits::max() - *invoke_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content = *invoke_tlv + *service_tlv; + const auto required = asn1::BerSpanWriter::tlv_size(outer_tag, content); + if (!required || *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, true, outer_tag, content) || + !writer.write_tlv_header( + asn1::BerClass::universal, false, 2, invoke_value) || + !write_positive_integer(writer, invoke_id) || + !writer.write_tlv( + asn1::BerClass::context_specific, + service_constructed, + service_tag, + service_value) || + writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +[[nodiscard]] wire::EncodeResult encode_reject( + const std::uint32_t invoke_id, + const MmsConfirmedRequestRejectReason reason, + const std::span destination) noexcept { + if (invoke_id > MmsPduSpanCodec::maximum_invoke_id || + static_cast(reason) > + static_cast(MmsConfirmedRequestRejectReason::invalid_argument)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto invoke_value = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(0, invoke_value); + const auto reason_tlv = asn1::BerSpanWriter::tlv_size(1, 1U); + if (!invoke_tlv || !reason_tlv || + *invoke_tlv > std::numeric_limits::max() - *reason_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content = *invoke_tlv + *reason_tlv; + const auto required = asn1::BerSpanWriter::tlv_size(4, content); + if (!required || *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, true, 4, content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, invoke_value) || + !write_positive_integer(writer, invoke_id) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 1, 1U) || + !writer.write_byte(static_cast(reason)) || + writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +} // namespace + +MmsWireConfirmedService MmsConfirmedPduView::service() const noexcept { + switch (service_tag) { + case 1: return MmsWireConfirmedService::get_name_list; + case 2: return MmsWireConfirmedService::identify; + case 4: return MmsWireConfirmedService::read; + case 5: return MmsWireConfirmedService::write; + case 6: return MmsWireConfirmedService::get_variable_access_attributes; + case 12: return MmsWireConfirmedService::get_named_variable_list_attributes; + case 77: return MmsWireConfirmedService::file_directory; + default: return MmsWireConfirmedService::unknown; + } +} + +bool MmsPduSpanCodec::try_decode_initiate_request_view( + const std::span bytes, + MmsInitiateView& request) noexcept { + return decode_initiate(bytes, 8, MmsWirePduKind::initiate_request, request); +} + +bool MmsPduSpanCodec::try_decode_initiate_response_view( + const std::span bytes, + MmsInitiateView& response) noexcept { + return decode_initiate(bytes, 9, MmsWirePduKind::initiate_response, response); +} + +wire::EncodeResult MmsPduSpanCodec::encode_default_initiate_response_into( + const std::span destination) noexcept { + if (destination.size() < kDefaultInitiateResponse.size()) { + return { + wire::EncodeStatus::buffer_too_small, + 0U, + kDefaultInitiateResponse.size()}; + } + std::copy( + kDefaultInitiateResponse.begin(), + kDefaultInitiateResponse.end(), + destination.begin()); + return { + wire::EncodeStatus::ok, + kDefaultInitiateResponse.size(), + kDefaultInitiateResponse.size()}; +} + +wire::EncodeResult MmsPduSpanCodec::encode_initiate_response_into( + const std::uint32_t maximum_mms_pdu_size, + const std::uint32_t maximum_outstanding_calling, + const std::uint32_t maximum_outstanding_called, + const std::uint32_t data_structure_nesting_level, + const std::span destination) noexcept { + if (maximum_mms_pdu_size < 64U || + maximum_mms_pdu_size > maximum_pdu_bytes || + maximum_outstanding_calling == 0U || + maximum_outstanding_called == 0U || + data_structure_nesting_level == 0U) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const std::array values{ + maximum_mms_pdu_size, + maximum_outstanding_calling, + maximum_outstanding_called, + data_structure_nesting_level}; + std::array value_sizes{}; + std::size_t content_size = 0U; + for (std::size_t index = 0U; index < values.size(); ++index) { + value_sizes[index] = positive_integer_size(values[index]); + const auto field = asn1::BerSpanWriter::tlv_size( + static_cast(index), value_sizes[index]); + if (!field || *field > std::numeric_limits::max() - content_size) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + content_size += *field; + } + const auto detail = asn1::BerSpanWriter::tlv_size(4, kInitiateResponseDetail.size()); + if (!detail || *detail > std::numeric_limits::max() - content_size) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + content_size += *detail; + const auto required = asn1::BerSpanWriter::tlv_size(9, content_size); + if (!required || destination.size() < *required) { + return { + required ? wire::EncodeStatus::buffer_too_small + : wire::EncodeStatus::value_out_of_range, + 0U, + required.value_or(0U)}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, true, 9, content_size)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + for (std::size_t index = 0U; index < values.size(); ++index) { + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, + false, + static_cast(index), + value_sizes[index]) || + !write_positive_integer(writer, values[index])) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, + true, + 4, + kInitiateResponseDetail.size()) || + !writer.write_bytes(kInitiateResponseDetail) || + writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +bool MmsPduSpanCodec::try_decode_confirmed_request_view( + const std::span bytes, + MmsConfirmedPduView& request) noexcept { + return decode_confirmed(bytes, 0, MmsWirePduKind::confirmed_request, request); +} + +bool MmsPduSpanCodec::try_decode_confirmed_response_view( + const std::span bytes, + MmsConfirmedPduView& response) noexcept { + return decode_confirmed(bytes, 1, MmsWirePduKind::confirmed_response, response); +} + +wire::EncodeResult MmsPduSpanCodec::encode_confirmed_request_into( + const std::uint32_t invoke_id, + const std::int32_t service_tag, + const bool service_constructed, + const std::span service_value, + const std::span destination) noexcept { + return encode_confirmed( + 0, invoke_id, service_tag, service_constructed, service_value, destination); +} + +wire::EncodeResult MmsPduSpanCodec::encode_confirmed_response_into( + const std::uint32_t invoke_id, + const std::int32_t service_tag, + const bool service_constructed, + const std::span service_value, + const std::span destination) noexcept { + return encode_confirmed( + 1, invoke_id, service_tag, service_constructed, service_value, destination); +} + +wire::EncodeResult MmsPduSpanCodec::encode_confirmed_request_reject_into( + const std::uint32_t invoke_id, + const MmsConfirmedRequestRejectReason reason, + const std::span destination) noexcept { + return encode_reject(invoke_id, reason, destination); +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/services_span.cpp b/src/mms/services_span.cpp new file mode 100644 index 000000000..e68152280 --- /dev/null +++ b/src/mms/services_span.cpp @@ -0,0 +1,971 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/services_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool visible_ascii( + const std::span value, + const bool allow_empty = false) noexcept { + if ((!allow_empty && value.empty()) || + value.size() > MmsServiceSpanCodec::maximum_identifier_bytes) { + return false; + } + for (const auto byte : value) { + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] bool visible_ascii( + const std::string_view value, + const bool allow_empty = false) noexcept { + if ((!allow_empty && value.empty()) || + value.size() > MmsServiceSpanCodec::maximum_identifier_bytes) { + return false; + } + for (const auto character : value) { + const auto byte = static_cast(character); + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] bool read_u32( + const asn1::BerTlvView& tlv, + std::uint32_t& value) noexcept { + const auto decoded = asn1::BerSpanReader::read_uint32(tlv); + if (!decoded) { + value = 0U; + return false; + } + value = *decoded; + return true; +} + +[[nodiscard]] bool valid_identifier_tlv( + const asn1::BerTlvView& tlv) noexcept { + return !tlv.constructed && + tlv.tag_class == asn1::BerClass::universal && + (tlv.tag_number == 22 || tlv.tag_number == 26) && + visible_ascii(tlv.value); +} + +[[nodiscard]] bool valid_mms_data_tlv( + const asn1::BerTlvView& tlv) noexcept { + if (tlv.tag_class != asn1::BerClass::context_specific) { + return false; + } + if (tlv.tag_number == 1 || tlv.tag_number == 2) { + return tlv.constructed; + } + return tlv.tag_number >= 3 && tlv.tag_number <= 17 && !tlv.constructed; +} + +[[nodiscard]] std::size_t minimal_unsigned_size(std::uint32_t value) noexcept { + std::size_t bytes = 1U; + while (value > 0xFFU) { + ++bytes; + value >>= 8U; + } + return bytes; +} + +[[nodiscard]] bool needs_positive_prefix( + const std::uint32_t value, + const std::size_t minimal_bytes) noexcept { + const auto shift = static_cast((minimal_bytes - 1U) * 8U); + return ((value >> shift) & 0x80U) != 0U; +} + +[[nodiscard]] std::size_t positive_integer_size(const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + return minimal + (needs_positive_prefix(value, minimal) ? 1U : 0U); +} + +[[nodiscard]] bool write_positive_integer( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + if (needs_positive_prefix(value, minimal) && !writer.write_byte(0x00U)) { + return false; + } + for (std::size_t index = minimal; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast( + (value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] std::optional add_size( + const std::optional left, + const std::optional right) noexcept { + if (!left || !right || *right > std::numeric_limits::max() - *left) { + return std::nullopt; + } + return *left + *right; +} + +struct ConfirmedResponseSizing final { + std::size_t invoke_value{}; + std::size_t outer_content{}; + std::size_t required{}; +}; + +[[nodiscard]] std::optional confirmed_response_sizing( + const std::uint32_t invoke_id, + const std::int32_t service_tag, + const std::size_t service_content) noexcept { + if (invoke_id > MmsPduSpanCodec::maximum_invoke_id || + service_tag < 0 || service_tag > 1'000'000) { + return std::nullopt; + } + const auto invoke_value = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(2, invoke_value); + const auto service_tlv = asn1::BerSpanWriter::tlv_size(service_tag, service_content); + const auto outer_content = add_size(invoke_tlv, service_tlv); + if (!outer_content) { + return std::nullopt; + } + const auto required = asn1::BerSpanWriter::tlv_size(1, *outer_content); + if (!required || *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return std::nullopt; + } + return ConfirmedResponseSizing{invoke_value, *outer_content, *required}; +} + +[[nodiscard]] bool write_confirmed_response_prefix( + asn1::BerSpanWriter& writer, + const ConfirmedResponseSizing& sizing, + const std::uint32_t invoke_id, + const std::int32_t service_tag, + const std::size_t service_content) noexcept { + return writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, sizing.outer_content) && + writer.write_tlv_header( + asn1::BerClass::universal, false, 2, sizing.invoke_value) && + write_positive_integer(writer, invoke_id) && + writer.write_tlv_header( + asn1::BerClass::context_specific, true, service_tag, service_content); +} + +[[nodiscard]] bool try_read_variable_definition( + const std::span variable_list, + const std::size_t wanted, + MmsObjectNameView& name) noexcept { + name = {}; + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < variable_list.size()) { + asn1::BerTlvView definition; + if (!asn1::BerSpanReader::try_read_tlv(variable_list, offset, definition) || + definition.tag_class != asn1::BerClass::universal || + definition.tag_number != 16 || !definition.constructed) { + return false; + } + asn1::BerTlvView wrapper; + if (!asn1::BerSpanReader::try_read_exact(definition.value, wrapper) || + wrapper.tag_class != asn1::BerClass::context_specific || + wrapper.tag_number != 0 || !wrapper.constructed) { + return false; + } + MmsObjectNameView decoded; + if (!MmsServiceSpanCodec::try_decode_object_name_view(wrapper.value, decoded)) { + return false; + } + if (current == wanted) { + name = decoded; + return true; + } + ++current; + } + return false; +} + +[[nodiscard]] bool try_read_access_result( + const std::span list, + const std::size_t wanted, + MmsReadAccessResultView& result) noexcept { + result = {}; + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < list.size()) { + const auto start = offset; + asn1::BerTlvView access; + if (!asn1::BerSpanReader::try_read_tlv(list, offset, access)) { + return false; + } + MmsReadAccessResultView decoded; + if (access.tag_class == asn1::BerClass::context_specific && + access.tag_number == 0 && !access.constructed) { + if (!read_u32(access, decoded.failure_code)) { + return false; + } + decoded.success = false; + } else if (valid_mms_data_tlv(access)) { + decoded.success = true; + decoded.encoded_data = list.subspan(start, offset - start); + } else { + return false; + } + if (current == wanted) { + result = decoded; + return true; + } + ++current; + } + return false; +} + +} // namespace + +bool MmsServiceSpanCodec::try_decode_object_name_view( + const std::span encoded_object_name, + MmsObjectNameView& name) noexcept { + name = {}; + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(encoded_object_name, outer) || + outer.tag_class != asn1::BerClass::context_specific) { + return false; + } + if (outer.tag_number == 0 && !outer.constructed) { + if (!visible_ascii(outer.value)) { + return false; + } + name.kind = MmsObjectNameViewKind::vmd_specific; + name.item = outer.value; + return true; + } + if (outer.tag_number == 2 && !outer.constructed) { + if (!visible_ascii(outer.value)) { + return false; + } + name.kind = MmsObjectNameViewKind::aa_specific; + name.item = outer.value; + return true; + } + if (outer.tag_number != 1 || !outer.constructed) { + return false; + } + + std::size_t offset = 0U; + asn1::BerTlvView domain; + asn1::BerTlvView item; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, domain) || + !asn1::BerSpanReader::try_read_tlv(outer.value, offset, item) || + offset != outer.value.size() || + !valid_identifier_tlv(domain) || !valid_identifier_tlv(item)) { + return false; + } + name.kind = MmsObjectNameViewKind::domain_specific; + name.domain = domain.value; + name.item = item.value; + return true; +} + +bool MmsGetNameListResponseView::try_identifier( + const std::size_t index, + std::span& identifier) const noexcept { + identifier = {}; + if (index >= identifier_count) { + return false; + } + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < identifier_list.size()) { + asn1::BerTlvView item; + if (!asn1::BerSpanReader::try_read_tlv(identifier_list, offset, item) || + !valid_identifier_tlv(item)) { + return false; + } + if (current == index) { + identifier = item.value; + return true; + } + ++current; + } + return false; +} + +bool MmsReadRequestView::try_variable( + const std::size_t index, + MmsObjectNameView& name) const noexcept { + if (index >= variable_count) { + name = {}; + return false; + } + return try_read_variable_definition(variable_list, index, name); +} + +bool MmsReadResponseView::try_result( + const std::size_t index, + MmsReadAccessResultView& result) const noexcept { + if (index >= result_count) { + result = {}; + return false; + } + return try_read_access_result(access_result_list, index, result); +} + +bool MmsServiceSpanCodec::try_decode_get_name_list_request( + const MmsConfirmedPduView& confirmed, + MmsGetNameListRequestView& request) noexcept { + request = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_request || + confirmed.service_tag != 1 || !confirmed.service_constructed) { + return false; + } + + bool have_class = false; + bool have_scope = false; + bool have_continuation = false; + std::size_t count = 0U; + std::size_t offset = 0U; + while (offset < confirmed.service_value.size()) { + if (count >= 3U) { + request = {}; + return false; + } + ++count; + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv( + confirmed.service_value, offset, field) || + field.tag_class != asn1::BerClass::context_specific) { + request = {}; + return false; + } + + if (field.tag_number == 0 && field.constructed) { + if (have_class) { + request = {}; + return false; + } + asn1::BerTlvView inner; + std::uint32_t value{}; + if (!asn1::BerSpanReader::try_read_exact(field.value, inner) || + inner.tag_class != asn1::BerClass::context_specific || + inner.tag_number != 0 || inner.constructed || + !read_u32(inner, value) || + (value != 0U && value != 2U && value != 9U)) { + request = {}; + return false; + } + request.object_class = static_cast(value); + have_class = true; + } else if (field.tag_number == 1 && field.constructed) { + if (have_scope) { + request = {}; + return false; + } + asn1::BerTlvView inner; + if (!asn1::BerSpanReader::try_read_exact(field.value, inner) || + inner.tag_class != asn1::BerClass::context_specific || + inner.constructed) { + request = {}; + return false; + } + switch (inner.tag_number) { + case 0: + if (!inner.value.empty()) { + request = {}; + return false; + } + request.scope = MmsNameScopeKind::vmd_specific; + break; + case 1: + if (!visible_ascii(inner.value)) { + request = {}; + return false; + } + request.scope = MmsNameScopeKind::domain_specific; + request.domain_id = inner.value; + break; + case 2: + if (!inner.value.empty()) { + request = {}; + return false; + } + request.scope = MmsNameScopeKind::aa_specific; + break; + default: + request = {}; + return false; + } + have_scope = true; + } else if (field.tag_number == 2 && !field.constructed) { + if (have_continuation || !visible_ascii(field.value)) { + request = {}; + return false; + } + request.continue_after = field.value; + have_continuation = true; + } else { + request = {}; + return false; + } + } + + if (!have_class || !have_scope) { + request = {}; + return false; + } + request.invoke_id = confirmed.invoke_id; + return true; +} + +bool MmsServiceSpanCodec::try_decode_get_name_list_request( + const std::span mms_pdu, + MmsGetNameListRequestView& request) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_request_view(mms_pdu, confirmed) && + try_decode_get_name_list_request(confirmed, request); +} + +bool MmsServiceSpanCodec::try_decode_get_name_list_response( + const MmsConfirmedPduView& confirmed, + MmsGetNameListResponseView& response) noexcept { + response = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_response || + confirmed.service_tag != 1 || !confirmed.service_constructed) { + return false; + } + + bool have_list = false; + bool have_more = false; + std::size_t offset = 0U; + while (offset < confirmed.service_value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv( + confirmed.service_value, offset, field) || + field.tag_class != asn1::BerClass::context_specific) { + response = {}; + return false; + } + + if (field.tag_number == 0 && field.constructed) { + if (have_list) { + response = {}; + return false; + } + std::size_t name_offset = 0U; + std::size_t name_count = 0U; + while (name_offset < field.value.size()) { + if (name_count >= maximum_identifiers) { + response = {}; + return false; + } + asn1::BerTlvView name; + if (!asn1::BerSpanReader::try_read_tlv( + field.value, name_offset, name) || + !valid_identifier_tlv(name)) { + response = {}; + return false; + } + ++name_count; + } + response.identifier_list = field.value; + response.identifier_count = name_count; + have_list = true; + } else if (field.tag_number == 1 && !field.constructed) { + if (have_more || field.value.size() != 1U) { + response = {}; + return false; + } + response.more_follows = field.value[0] != 0U; + have_more = true; + } else { + response = {}; + return false; + } + } + + if (!have_list) { + response = {}; + return false; + } + response.invoke_id = confirmed.invoke_id; + return true; +} + +bool MmsServiceSpanCodec::try_decode_get_name_list_response( + const std::span mms_pdu, + MmsGetNameListResponseView& response) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_response_view(mms_pdu, confirmed) && + try_decode_get_name_list_response(confirmed, response); +} + +wire::EncodeResult MmsServiceSpanCodec::encode_get_name_list_response_into( + const std::uint32_t invoke_id, + const std::span names, + const bool more_follows, + const std::span destination) noexcept { + if (invoke_id > MmsPduSpanCodec::maximum_invoke_id || + names.size() > maximum_identifiers) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + std::size_t names_content = 0U; + for (const auto name : names) { + if (!visible_ascii(name)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto encoded = asn1::BerSpanWriter::tlv_size(26, name.size()); + if (!encoded || *encoded > std::numeric_limits::max() - names_content) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + names_content += *encoded; + } + + const auto list_tlv = asn1::BerSpanWriter::tlv_size(0, names_content); + const auto more_tlv = asn1::BerSpanWriter::tlv_size(1, 1U); + const auto service_content = add_size(list_tlv, more_tlv); + if (!service_content) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto service_tlv = asn1::BerSpanWriter::tlv_size(1, *service_content); + + const auto invoke_value = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(2, invoke_value); + const auto outer_content = add_size(invoke_tlv, service_tlv); + if (!outer_content) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto required = asn1::BerSpanWriter::tlv_size(1, *outer_content); + if (!required || *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < *required) { + return {wire::EncodeStatus::buffer_too_small, 0U, *required}; + } + + asn1::BerSpanWriter writer{destination.first(*required)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, *outer_content) || + !writer.write_tlv_header( + asn1::BerClass::universal, false, 2, invoke_value) || + !write_positive_integer(writer, invoke_id) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, *service_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 0, names_content)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + + for (const auto name : names) { + if (!writer.write_tlv_header( + asn1::BerClass::universal, false, 26, name.size())) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + for (const auto character : name) { + if (!writer.write_byte(static_cast( + static_cast(character)))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + } + } + + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 1, 1U) || + !writer.write_byte(more_follows ? 0xFFU : 0x00U) || + writer.size() != *required) { + return {wire::EncodeStatus::value_out_of_range, 0U, *required}; + } + return {wire::EncodeStatus::ok, *required, *required}; +} + +bool MmsServiceSpanCodec::try_decode_variable_access_attributes_request( + const MmsConfirmedPduView& confirmed, + MmsVariableAccessAttributesRequestView& request) noexcept { + request = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_request || + confirmed.service_tag != 6 || !confirmed.service_constructed) { + return false; + } + asn1::BerTlvView wrapper; + if (!asn1::BerSpanReader::try_read_exact(confirmed.service_value, wrapper) || + wrapper.tag_class != asn1::BerClass::context_specific || + wrapper.tag_number != 0 || !wrapper.constructed || + !try_decode_object_name_view(wrapper.value, request.name)) { + request = {}; + return false; + } + request.invoke_id = confirmed.invoke_id; + return true; +} + +bool MmsServiceSpanCodec::try_decode_variable_access_attributes_request( + const std::span mms_pdu, + MmsVariableAccessAttributesRequestView& request) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_request_view(mms_pdu, confirmed) && + try_decode_variable_access_attributes_request(confirmed, request); +} + +bool MmsServiceSpanCodec::try_decode_variable_access_attributes_response( + const MmsConfirmedPduView& confirmed, + MmsVariableAccessAttributesResponseView& response) noexcept { + response = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_response || + confirmed.service_tag != 6 || !confirmed.service_constructed) { + return false; + } + bool have_deletable = false; + bool have_type = false; + std::size_t offset = 0U; + while (offset < confirmed.service_value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(confirmed.service_value, offset, field) || + field.tag_class != asn1::BerClass::context_specific) { + response = {}; + return false; + } + if (field.tag_number == 0 && !field.constructed) { + if (have_deletable || field.value.size() != 1U) { + response = {}; + return false; + } + response.mms_deletable = field.value[0] != 0U; + have_deletable = true; + } else if (field.tag_number == 2 && field.constructed) { + if (have_type) { + response = {}; + return false; + } + asn1::BerTlvView type; + if (!asn1::BerSpanReader::try_read_exact(field.value, type) || + type.tag_class != asn1::BerClass::context_specific) { + response = {}; + return false; + } + response.type_specification = field.value; + have_type = true; + } else { + response = {}; + return false; + } + } + if (!have_type) { + response = {}; + return false; + } + response.invoke_id = confirmed.invoke_id; + return true; +} + +bool MmsServiceSpanCodec::try_decode_variable_access_attributes_response( + const std::span mms_pdu, + MmsVariableAccessAttributesResponseView& response) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_response_view(mms_pdu, confirmed) && + try_decode_variable_access_attributes_response(confirmed, response); +} + +wire::EncodeResult MmsServiceSpanCodec::encode_variable_access_attributes_response_into( + const std::uint32_t invoke_id, + const bool mms_deletable, + const std::span encoded_type_specification, + const std::span destination) noexcept { + asn1::BerTlvView type; + if (!asn1::BerSpanReader::try_read_exact(encoded_type_specification, type) || + type.tag_class != asn1::BerClass::context_specific) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto deletable_tlv = asn1::BerSpanWriter::tlv_size(0, 1U); + const auto type_wrapper = asn1::BerSpanWriter::tlv_size( + 2, encoded_type_specification.size()); + const auto service_content = add_size(deletable_tlv, type_wrapper); + if (!service_content) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto sizing = confirmed_response_sizing(invoke_id, 6, *service_content); + if (!sizing) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < sizing->required) { + return {wire::EncodeStatus::buffer_too_small, 0U, sizing->required}; + } + + asn1::BerSpanWriter writer{destination.first(sizing->required)}; + if (!write_confirmed_response_prefix( + writer, *sizing, invoke_id, 6, *service_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, 1U) || + !writer.write_byte(mms_deletable ? 0xFFU : 0x00U) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 2, + encoded_type_specification.size()) || + !writer.write_bytes(encoded_type_specification) || + writer.size() != sizing->required) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + return {wire::EncodeStatus::ok, sizing->required, sizing->required}; +} + +bool MmsServiceSpanCodec::try_decode_read_request( + const MmsConfirmedPduView& confirmed, + MmsReadRequestView& request) noexcept { + request = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_request || + confirmed.service_tag != 4 || !confirmed.service_constructed) { + return false; + } + + bool have_flag = false; + bool have_specification = false; + std::size_t offset = 0U; + while (offset < confirmed.service_value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(confirmed.service_value, offset, field) || + field.tag_class != asn1::BerClass::context_specific) { + request = {}; + return false; + } + if (field.tag_number == 0 && !field.constructed) { + if (have_flag || field.value.size() != 1U) { + request = {}; + return false; + } + request.specification_with_result = field.value[0] != 0U; + have_flag = true; + } else if (field.tag_number == 1 && field.constructed) { + if (have_specification) { + request = {}; + return false; + } + asn1::BerTlvView list; + if (!asn1::BerSpanReader::try_read_exact(field.value, list) || + list.tag_class != asn1::BerClass::context_specific || + list.tag_number != 0 || !list.constructed) { + request = {}; + return false; + } + std::size_t variable_offset = 0U; + std::size_t count = 0U; + while (variable_offset < list.value.size()) { + if (count >= maximum_variables) { + request = {}; + return false; + } + const auto before = variable_offset; + asn1::BerTlvView definition; + if (!asn1::BerSpanReader::try_read_tlv( + list.value, variable_offset, definition)) { + request = {}; + return false; + } + MmsObjectNameView ignored; + if (!try_read_variable_definition( + list.value.subspan(before, variable_offset - before), 0U, ignored)) { + request = {}; + return false; + } + ++count; + } + if (count == 0U) { + request = {}; + return false; + } + request.variable_list = list.value; + request.variable_count = count; + have_specification = true; + } else { + request = {}; + return false; + } + } + if (!have_specification) { + request = {}; + return false; + } + request.invoke_id = confirmed.invoke_id; + return true; +} + +bool MmsServiceSpanCodec::try_decode_read_request( + const std::span mms_pdu, + MmsReadRequestView& request) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_request_view(mms_pdu, confirmed) && + try_decode_read_request(confirmed, request); +} + +bool MmsServiceSpanCodec::try_decode_read_response( + const MmsConfirmedPduView& confirmed, + MmsReadResponseView& response) noexcept { + response = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_response || + confirmed.service_tag != 4 || !confirmed.service_constructed) { + return false; + } + bool have_list = false; + bool have_optional_specification = false; + std::size_t offset = 0U; + while (offset < confirmed.service_value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(confirmed.service_value, offset, field) || + field.tag_class != asn1::BerClass::context_specific) { + response = {}; + return false; + } + if (field.tag_number == 0 && field.constructed) { + if (have_optional_specification) { + response = {}; + return false; + } + have_optional_specification = true; + } else if (field.tag_number == 1 && field.constructed) { + if (have_list) { + response = {}; + return false; + } + std::size_t result_offset = 0U; + std::size_t count = 0U; + while (result_offset < field.value.size()) { + if (count >= maximum_variables) { + response = {}; + return false; + } + const auto start = result_offset; + asn1::BerTlvView result; + if (!asn1::BerSpanReader::try_read_tlv( + field.value, result_offset, result)) { + response = {}; + return false; + } + if (result.tag_class == asn1::BerClass::context_specific && + result.tag_number == 0 && !result.constructed) { + std::uint32_t ignored{}; + if (!read_u32(result, ignored)) { + response = {}; + return false; + } + } else if (!valid_mms_data_tlv(result)) { + response = {}; + return false; + } + if (result_offset <= start) { + response = {}; + return false; + } + ++count; + } + if (count == 0U) { + response = {}; + return false; + } + response.access_result_list = field.value; + response.result_count = count; + have_list = true; + } else { + response = {}; + return false; + } + } + if (!have_list) { + response = {}; + return false; + } + response.invoke_id = confirmed.invoke_id; + return true; +} + +bool MmsServiceSpanCodec::try_decode_read_response( + const std::span mms_pdu, + MmsReadResponseView& response) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_response_view(mms_pdu, confirmed) && + try_decode_read_response(confirmed, response); +} + +wire::EncodeResult MmsServiceSpanCodec::encode_read_response_into( + const std::uint32_t invoke_id, + const std::span results, + const std::span destination) noexcept { + if (results.empty() || results.size() > maximum_variables) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + std::size_t list_content = 0U; + for (const auto& result : results) { + std::size_t encoded_size = 0U; + if (result.success) { + asn1::BerTlvView data; + if (!asn1::BerSpanReader::try_read_exact(result.encoded_data, data) || + !valid_mms_data_tlv(data)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + encoded_size = result.encoded_data.size(); + } else { + const auto failure_value = positive_integer_size(result.failure_code); + const auto failure_tlv = asn1::BerSpanWriter::tlv_size(0, failure_value); + if (!failure_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + encoded_size = *failure_tlv; + } + if (encoded_size > std::numeric_limits::max() - list_content) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + list_content += encoded_size; + } + + const auto list_tlv = asn1::BerSpanWriter::tlv_size(1, list_content); + if (!list_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto sizing = confirmed_response_sizing(invoke_id, 4, *list_tlv); + if (!sizing) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < sizing->required) { + return {wire::EncodeStatus::buffer_too_small, 0U, sizing->required}; + } + + asn1::BerSpanWriter writer{destination.first(sizing->required)}; + if (!write_confirmed_response_prefix( + writer, *sizing, invoke_id, 4, *list_tlv) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, list_content)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + for (const auto& result : results) { + if (result.success) { + if (!writer.write_bytes(result.encoded_data)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + } else { + const auto failure_value = positive_integer_size(result.failure_code); + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, failure_value) || + !write_positive_integer(writer, result.failure_code)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + } + } + if (writer.size() != sizing->required) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + return {wire::EncodeStatus::ok, sizing->required, sizing->required}; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_brcb_checkpoint_store.cpp b/src/mms/static_brcb_checkpoint_store.cpp new file mode 100644 index 000000000..0afc180f2 --- /dev/null +++ b/src/mms/static_brcb_checkpoint_store.cpp @@ -0,0 +1,425 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_checkpoint_store.hpp" + +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::array kHeaderMagic{ + 'A','R','B','R','C','B','P','1'}; +constexpr std::array kFooterMagic{ + 'A','R','C','O','M','M','I','T'}; +constexpr std::size_t kCrcChunkBytes = 256U; + +struct BankInfo final { + bool valid{}; + bool io_error{}; + std::uint64_t generation{}; + std::uint32_t payload_bytes{}; + std::uint32_t payload_crc{}; +}; + +void write_u16( + const std::span destination, + const std::size_t offset, + const std::uint16_t value) noexcept { + destination[offset] = static_cast((value >> 8U) & 0xFFU); + destination[offset + 1U] = static_cast(value & 0xFFU); +} + +void write_u32( + const std::span destination, + const std::size_t offset, + const std::uint32_t value) noexcept { + for (std::size_t index = 0U; index < 4U; ++index) { + const auto shift = static_cast((3U - index) * 8U); + destination[offset + index] = static_cast((value >> shift) & 0xFFU); + } +} + +void write_u64( + const std::span destination, + const std::size_t offset, + const std::uint64_t value) noexcept { + for (std::size_t index = 0U; index < 8U; ++index) { + const auto shift = static_cast((7U - index) * 8U); + destination[offset + index] = static_cast((value >> shift) & 0xFFU); + } +} + +[[nodiscard]] std::uint16_t read_u16( + const std::span source, + const std::size_t offset) noexcept { + return static_cast( + (static_cast(source[offset]) << 8U) | + static_cast(source[offset + 1U])); +} + +[[nodiscard]] std::uint32_t read_u32( + const std::span source, + const std::size_t offset) noexcept { + std::uint32_t value = 0U; + for (std::size_t index = 0U; index < 4U; ++index) { + value = static_cast((value << 8U) | source[offset + index]); + } + return value; +} + +[[nodiscard]] std::uint64_t read_u64( + const std::span source, + const std::size_t offset) noexcept { + std::uint64_t value = 0U; + for (std::size_t index = 0U; index < 8U; ++index) { + value = (value << 8U) | source[offset + index]; + } + return value; +} + +[[nodiscard]] std::uint32_t crc32_update( + std::uint32_t state, + const std::span bytes) noexcept { + for (const auto byte : bytes) { + state ^= byte; + for (std::uint8_t bit = 0U; bit < 8U; ++bit) { + const auto mask = static_cast( + 0U - static_cast(state & 1U)); + state = (state >> 1U) ^ (0xEDB88320U & mask); + } + } + return state; +} + +[[nodiscard]] std::uint32_t crc32( + const std::span bytes) noexcept { + return ~crc32_update(0xFFFF'FFFFU, bytes); +} + +[[nodiscard]] bool generation_newer( + const std::uint64_t left, + const std::uint64_t right) noexcept { + if (left == right) { + return false; + } + return static_cast(left - right) < (std::uint64_t{1U} << 63U); +} + +[[nodiscard]] std::uint64_t next_generation(const std::uint64_t current) noexcept { + return current == 0U || current == std::numeric_limits::max() + ? 1U + : current + 1U; +} + +[[nodiscard]] std::size_t bank_offset( + const std::size_t bank_index, + const std::size_t bank_bytes) noexcept { + return bank_index * bank_bytes; +} + +[[nodiscard]] bool read_payload_crc( + const MmsStaticBrcbStorageBackend& backend, + const std::size_t payload_offset, + const std::size_t payload_bytes, + std::uint32_t& result) noexcept { + std::array chunk{}; + std::size_t consumed = 0U; + std::uint32_t state = 0xFFFF'FFFFU; + while (consumed < payload_bytes) { + const auto remaining = payload_bytes - consumed; + const auto bytes = std::min(remaining, chunk.size()); + if (!backend.read( + backend.context, + payload_offset + consumed, + std::span{chunk}.first(bytes))) { + return false; + } + state = crc32_update( + state, + std::span{chunk}.first(bytes)); + consumed += bytes; + } + result = ~state; + return true; +} + +[[nodiscard]] BankInfo inspect_bank( + const MmsStaticBrcbStorageBackend& backend, + const std::size_t bank_index, + const std::size_t bank_bytes) noexcept { + BankInfo info; + std::array header{}; + std::array footer{}; + const auto base = bank_offset(bank_index, bank_bytes); + const auto footer_offset = base + bank_bytes - footer.size(); + if (!backend.read(backend.context, base, header) || + !backend.read(backend.context, footer_offset, footer)) { + info.io_error = true; + return info; + } + + if (!std::equal(kHeaderMagic.begin(), kHeaderMagic.end(), header.begin()) || + read_u16(header, 8U) != MmsStaticBrcbCheckpointStore::format_version || + read_u16(header, 10U) != MmsStaticBrcbCheckpointStore::record_header_bytes || + crc32(std::span{header}.first(36U)) != read_u32(header, 36U) || + !std::equal(kFooterMagic.begin(), kFooterMagic.end(), footer.begin()) || + crc32(std::span{footer}.first(28U)) != read_u32(footer, 28U)) { + return info; + } + for (std::size_t index = 28U; index < 36U; ++index) { + if (header[index] != 0U) { + return info; + } + } + for (std::size_t index = 24U; index < 28U; ++index) { + if (footer[index] != 0U) { + return info; + } + } + + const auto generation = read_u64(header, 12U); + const auto payload_bytes = read_u32(header, 20U); + const auto payload_crc = read_u32(header, 24U); + if (generation == 0U || payload_bytes == 0U || + generation != read_u64(footer, 8U) || + payload_bytes != read_u32(footer, 16U) || + payload_crc != read_u32(footer, 20U) || + static_cast(payload_bytes) > + bank_bytes - MmsStaticBrcbCheckpointStore::record_header_bytes - + MmsStaticBrcbCheckpointStore::record_footer_bytes) { + return info; + } + + std::uint32_t measured_crc = 0U; + if (!read_payload_crc( + backend, + base + MmsStaticBrcbCheckpointStore::record_header_bytes, + payload_bytes, + measured_crc)) { + info.io_error = true; + return info; + } + if (measured_crc != payload_crc) { + return info; + } + + info.valid = true; + info.generation = generation; + info.payload_bytes = payload_bytes; + info.payload_crc = payload_crc; + return info; +} + +void build_header( + const std::uint64_t generation, + const std::uint32_t payload_bytes, + const std::uint32_t payload_crc, + std::span header) noexcept { + std::fill(header.begin(), header.end(), std::uint8_t{0U}); + std::copy(kHeaderMagic.begin(), kHeaderMagic.end(), header.begin()); + write_u16(header, 8U, MmsStaticBrcbCheckpointStore::format_version); + write_u16( + header, + 10U, + static_cast(MmsStaticBrcbCheckpointStore::record_header_bytes)); + write_u64(header, 12U, generation); + write_u32(header, 20U, payload_bytes); + write_u32(header, 24U, payload_crc); + write_u32(header, 36U, crc32(std::span{header}.first(36U))); +} + +void build_footer( + const std::uint64_t generation, + const std::uint32_t payload_bytes, + const std::uint32_t payload_crc, + std::span footer) noexcept { + std::fill(footer.begin(), footer.end(), std::uint8_t{0U}); + std::copy(kFooterMagic.begin(), kFooterMagic.end(), footer.begin()); + write_u64(footer, 8U, generation); + write_u32(footer, 16U, payload_bytes); + write_u32(footer, 20U, payload_crc); + write_u32(footer, 28U, crc32(std::span{footer}.first(28U))); +} + +} // namespace + +bool MmsStaticBrcbCheckpointStore::valid() const noexcept { + return backend_.valid() && + bank_bytes_ > record_header_bytes + record_footer_bytes && + bank_bytes_ <= backend_.capacity_bytes / bank_count; +} + +MmsStaticBrcbCheckpointResult MmsStaticBrcbCheckpointStore::checkpoint( + const MmsStaticBrcbRuntime& runtime, + const std::span state_buffer) const noexcept { + MmsStaticBrcbCheckpointResult result; + if (!valid()) { + result.status = MmsStaticBrcbCheckpointStatus::invalid_backend; + return result; + } + + const auto state = MmsStaticBrcbStateCodec::encode(runtime, state_buffer); + result.state_status = state.status; + result.required_state_bytes = state.required_bytes; + if (!state.success()) { + result.status = state.status == MmsStaticBrcbStateStatus::buffer_too_small + ? MmsStaticBrcbCheckpointStatus::state_buffer_too_small + : MmsStaticBrcbCheckpointStatus::state_encode_failed; + return result; + } + if (state.bytes_written > maximum_state_bytes() || + state.bytes_written > std::numeric_limits::max()) { + result.status = MmsStaticBrcbCheckpointStatus::invalid_layout; + result.state_bytes = state.bytes_written; + return result; + } + + const auto first = inspect_bank(backend_, 0U, bank_bytes_); + const auto second = inspect_bank(backend_, 1U, bank_bytes_); + if (first.io_error || second.io_error) { + result.status = MmsStaticBrcbCheckpointStatus::storage_failure; + return result; + } + + std::size_t target = 0U; + std::uint64_t current_generation = 0U; + if (first.valid && second.valid) { + const auto newest = generation_newer(second.generation, first.generation) ? 1U : 0U; + target = 1U - newest; + current_generation = newest == 0U ? first.generation : second.generation; + } else if (first.valid) { + target = 1U; + current_generation = first.generation; + } else if (second.valid) { + target = 0U; + current_generation = second.generation; + } + const auto generation = next_generation(current_generation); + const auto payload_bytes = static_cast(state.bytes_written); + const auto payload_crc = crc32(state_buffer.first(state.bytes_written)); + + std::array header{}; + std::array footer{}; + build_header(generation, payload_bytes, payload_crc, header); + build_footer(generation, payload_bytes, payload_crc, footer); + + const auto base = bank_offset(target, bank_bytes_); + const auto footer_offset = base + bank_bytes_ - footer.size(); + if (!backend_.erase(backend_.context, base, bank_bytes_) || + !backend_.sync(backend_.context) || + !backend_.write(backend_.context, base, header) || + !backend_.write( + backend_.context, + base + record_header_bytes, + state_buffer.first(state.bytes_written)) || + !backend_.sync(backend_.context) || + !backend_.write(backend_.context, footer_offset, footer) || + !backend_.sync(backend_.context)) { + result.status = MmsStaticBrcbCheckpointStatus::storage_failure; + result.generation = generation; + result.bank_index = target; + result.state_bytes = state.bytes_written; + return result; + } + + result.status = MmsStaticBrcbCheckpointStatus::ok; + result.generation = generation; + result.bank_index = target; + result.state_bytes = state.bytes_written; + return result; +} + +MmsStaticBrcbCheckpointResult MmsStaticBrcbCheckpointStore::restore( + MmsStaticBrcbRuntime& runtime, + const std::span state_buffer) const noexcept { + MmsStaticBrcbCheckpointResult result; + if (!valid()) { + result.status = MmsStaticBrcbCheckpointStatus::invalid_backend; + return result; + } + + const auto first = inspect_bank(backend_, 0U, bank_bytes_); + const auto second = inspect_bank(backend_, 1U, bank_bytes_); + const bool first_usable = first.valid && !first.io_error; + const bool second_usable = second.valid && !second.io_error; + if (!first_usable && !second_usable) { + result.status = first.io_error || second.io_error + ? MmsStaticBrcbCheckpointStatus::storage_failure + : MmsStaticBrcbCheckpointStatus::no_checkpoint; + return result; + } + + std::size_t primary_index = 0U; + BankInfo primary = first; + bool fallback_available = false; + std::size_t fallback_index = 0U; + BankInfo fallback{}; + if (first_usable && second_usable) { + if (generation_newer(second.generation, first.generation)) { + primary_index = 1U; + primary = second; + fallback_index = 0U; + fallback = first; + } else { + primary_index = 0U; + primary = first; + fallback_index = 1U; + fallback = second; + } + fallback_available = true; + } else if (second_usable) { + primary_index = 1U; + primary = second; + } + + const auto load_bank = [this, state_buffer, &result]( + const std::size_t selected, + const BankInfo& info) noexcept { + result.generation = info.generation; + result.bank_index = selected; + result.state_bytes = info.payload_bytes; + result.required_state_bytes = info.payload_bytes; + if (state_buffer.size() < info.payload_bytes) { + return MmsStaticBrcbCheckpointStatus::state_buffer_too_small; + } + const auto base = bank_offset(selected, bank_bytes_); + auto payload = state_buffer.first(info.payload_bytes); + if (!backend_.read( + backend_.context, + base + record_header_bytes, + payload) || + crc32(payload) != info.payload_crc) { + return MmsStaticBrcbCheckpointStatus::storage_failure; + } + return MmsStaticBrcbCheckpointStatus::ok; + }; + + auto load_status = load_bank(primary_index, primary); + if (load_status == MmsStaticBrcbCheckpointStatus::storage_failure && + fallback_available) { + load_status = load_bank(fallback_index, fallback); + } + if (load_status != MmsStaticBrcbCheckpointStatus::ok) { + result.status = load_status; + return result; + } + + const auto payload = state_buffer.first(result.state_bytes); + const auto restored = MmsStaticBrcbStateCodec::restore(runtime, payload); + result.state_status = restored.status; + if (!restored.success()) { + result.status = MmsStaticBrcbCheckpointStatus::state_restore_failed; + return result; + } + result.status = MmsStaticBrcbCheckpointStatus::ok; + return result; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_brcb_connection.cpp b/src/mms/static_brcb_connection.cpp new file mode 100644 index 000000000..e7f3b9b66 --- /dev/null +++ b/src/mms/static_brcb_connection.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_connection.hpp" + +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool add_size( + const std::size_t base, + const std::size_t extra, + std::size_t& total) noexcept { + if (extra > std::numeric_limits::max() - base) { + total = 0U; + return false; + } + total = base + extra; + return true; +} + +[[nodiscard]] MmsStaticBrcbConnectionResult make_result( + const MmsStaticBrcbConnectionStatus status, + const MmsStaticBrcbEntryView* entry = nullptr) noexcept { + MmsStaticBrcbConnectionResult result; + result.status = status; + if (entry != nullptr) { + if (entry->entry_id.size() == result.entry_id.size()) { + std::copy(entry->entry_id.begin(), entry->entry_id.end(), result.entry_id.begin()); + } + result.sequence_number = entry->sequence_number; + result.buffer_overflow = entry->buffer_overflow; + } + return result; +} + +[[nodiscard]] bool same_owner( + const std::span left, + const std::span right) noexcept { + return left.size() == right.size() && !left.empty() && + std::equal(left.begin(), left.end(), right.begin()); +} + +} // namespace + +MmsStaticBrcbConnectionResult MmsStaticBrcbConnection::poll( + const MmsStaticConnectionRuntime& connection, + MmsStaticBrcbControl& control, + MmsStaticBrcbRuntime& reports, + const std::uint64_t now_ms, + const std::span response, + const std::span workspace) noexcept { + if (connection.state() != MmsStaticConnectionState::established || + connection.mms_presentation_context_id() == 0U) { + return make_result(MmsStaticBrcbConnectionStatus::not_established); + } + + const auto control_state = control.state(now_ms); + if (!control_state.report_enabled) { + return make_result(MmsStaticBrcbConnectionStatus::reporting_disabled); + } + + const auto access = connection.access_context(); + if (!control.valid() || !control_state.reserved || !control_state.owner_connected || + control_state.association_id == 0U || + access.association_id != control_state.association_id || + !same_owner(access.owner, control_state.owner)) { + return make_result(MmsStaticBrcbConnectionStatus::access_denied); + } + + MmsStaticBrcbEntryView entry; + if (!reports.front(entry)) { + return make_result(MmsStaticBrcbConnectionStatus::no_report_available); + } + + const auto mms_limit = connection.negotiated_mms_pdu_size(); + if (mms_limit == 0U || entry.mms_pdu.size() > mms_limit) { + return make_result(MmsStaticBrcbConnectionStatus::peer_limit_exceeded, &entry); + } + + const auto p_data = osi::PresentationSpanCodec::encode_p_data_into( + entry.mms_pdu, + workspace, + connection.mms_presentation_context_id(), + true); + if (!p_data.success()) { + if (p_data.status != wire::EncodeStatus::buffer_too_small) { + return make_result(MmsStaticBrcbConnectionStatus::frame_encode_failed, &entry); + } + std::size_t final_required{}; + if (!add_size(p_data.required_bytes, 7U, final_required)) { + return make_result(MmsStaticBrcbConnectionStatus::frame_encode_failed, &entry); + } + auto result = make_result( + MmsStaticBrcbConnectionStatus::workspace_too_small, &entry); + result.required_workspace_bytes = final_required; + result.required_response_bytes = final_required; + return result; + } + + std::size_t cotp_required{}; + if (!add_size(p_data.bytes_written, 3U, cotp_required)) { + return make_result(MmsStaticBrcbConnectionStatus::frame_encode_failed, &entry); + } + const auto tpdu_limit = connection.negotiated_tpdu_size_bytes(); + if (tpdu_limit == 0U || cotp_required > tpdu_limit) { + return make_result(MmsStaticBrcbConnectionStatus::peer_limit_exceeded, &entry); + } + + std::size_t final_required{}; + if (!add_size(cotp_required, osi::TpktSpanCodec::header_length, final_required)) { + return make_result(MmsStaticBrcbConnectionStatus::frame_encode_failed, &entry); + } + if (response.size() < final_required) { + auto result = make_result( + MmsStaticBrcbConnectionStatus::response_buffer_too_small, &entry); + result.required_response_bytes = final_required; + return result; + } + if (workspace.size() < final_required) { + auto result = make_result( + MmsStaticBrcbConnectionStatus::workspace_too_small, &entry); + result.required_response_bytes = final_required; + result.required_workspace_bytes = final_required; + return result; + } + + const auto cotp = osi::CotpSpanCodec::encode_data_into( + workspace.first(p_data.bytes_written), + response.first(final_required - osi::TpktSpanCodec::header_length)); + if (!cotp.success()) { + return make_result(MmsStaticBrcbConnectionStatus::frame_encode_failed, &entry); + } + + const auto tpkt = osi::TpktSpanCodec::encode_into( + response.first(cotp.bytes_written), + workspace.first(final_required)); + if (!tpkt.success() || tpkt.bytes_written != final_required) { + return make_result(MmsStaticBrcbConnectionStatus::frame_encode_failed, &entry); + } + + auto result = make_result(MmsStaticBrcbConnectionStatus::response_ready, &entry); + std::copy_n(workspace.begin(), tpkt.bytes_written, response.begin()); + result.bytes_written = tpkt.bytes_written; + result.required_response_bytes = final_required; + result.required_workspace_bytes = final_required; + return result; +} + +bool MmsStaticBrcbConnection::commit_sent( + const MmsStaticConnectionRuntime& connection, + MmsStaticBrcbControl& control, + MmsStaticBrcbRuntime& reports, + const std::uint64_t now_ms, + const MmsStaticBrcbConnectionResult& staged) noexcept { + if (!staged.response_ready() || + connection.state() != MmsStaticConnectionState::established || + connection.mms_presentation_context_id() == 0U || !control.valid()) { + return false; + } + + const auto control_state = control.state(now_ms); + if (!control_state.report_enabled || !control_state.reserved || + !control_state.owner_connected || control_state.association_id == 0U) { + return false; + } + + const auto access = connection.access_context(); + if (access.association_id != control_state.association_id || + !same_owner(access.owner, control_state.owner)) { + return false; + } + + return reports.commit_delivery(staged.entry_id) == MmsStaticBrcbStatus::ok; +} + +} // namespace ar::iec61850::mms \ No newline at end of file diff --git a/src/mms/static_brcb_control.cpp b/src/mms/static_brcb_control.cpp new file mode 100644 index 000000000..3aeb051ce --- /dev/null +++ b/src/mms/static_brcb_control.cpp @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_control.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] constexpr std::uint64_t reservation_deadline( + const std::uint64_t now_ms, + const std::uint32_t seconds) noexcept { + constexpr auto kMax = std::numeric_limits::max(); + const auto seconds64 = static_cast(seconds); + if (seconds64 > kMax / 1'000U) { + return kMax; + } + const auto duration = seconds64 * 1'000U; + return duration > kMax - now_ms ? kMax : now_ms + duration; +} + +} // namespace + +bool MmsStaticBrcbControl::same_owner( + const MmsStaticBrcbClientIdentity& client) const noexcept { + return client.valid() && reserved_ && client.owner_size == owner_size_ && + std::equal( + client.owner.begin(), + client.owner.begin() + static_cast(client.owner_size), + owner_.begin()); +} + +bool MmsStaticBrcbControl::owns_live_association( + const MmsStaticBrcbClientIdentity& client) const noexcept { + return same_owner(client) && owner_connected_ && + client.association_id == association_id_; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::map_runtime_status( + const MmsStaticBrcbStatus status) const noexcept { + switch (status) { + case MmsStaticBrcbStatus::ok: + return MmsStaticBrcbControlStatus::ok; + case MmsStaticBrcbStatus::entry_not_found: + return MmsStaticBrcbControlStatus::entry_not_found; + case MmsStaticBrcbStatus::temporarily_unavailable: + case MmsStaticBrcbStatus::no_report_due: + case MmsStaticBrcbStatus::stale_plan: + return MmsStaticBrcbControlStatus::temporarily_unavailable; + case MmsStaticBrcbStatus::invalid_runtime: + case MmsStaticBrcbStatus::invalid_definition: + case MmsStaticBrcbStatus::member_out_of_range: + case MmsStaticBrcbStatus::trigger_not_selected: + case MmsStaticBrcbStatus::data_set_not_found: + case MmsStaticBrcbStatus::object_not_found: + case MmsStaticBrcbStatus::workspace_too_small: + case MmsStaticBrcbStatus::response_buffer_too_small: + case MmsStaticBrcbStatus::slot_too_small: + case MmsStaticBrcbStatus::backend_failure: + case MmsStaticBrcbStatus::report_encode_failed: + return MmsStaticBrcbControlStatus::backend_failure; + } + return MmsStaticBrcbControlStatus::backend_failure; +} + +void MmsStaticBrcbControl::clear_reservation() noexcept { + owner_.fill(0U); + owner_size_ = 0U; + association_id_ = 0U; + reservation_expires_at_ms_ = 0U; + resv_tms_seconds_ = 0U; + reserved_ = false; + owner_connected_ = false; +} + +void MmsStaticBrcbControl::tick(const std::uint64_t now_ms) noexcept { + if (!reserved_ || owner_connected_ || resv_tms_seconds_ == 0U) { + return; + } + if (now_ms < reservation_expires_at_ms_) { + return; + } + if (reports_ != nullptr && reports_->valid() && reports_->enabled()) { + static_cast(reports_->set_enabled(false)); + } + clear_reservation(); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::reserve( + const MmsStaticBrcbClientIdentity& client, + const std::uint32_t resv_tms_seconds, + const std::uint64_t now_ms) noexcept { + if (!valid()) { + return MmsStaticBrcbControlStatus::invalid_runtime; + } + if (!client.valid()) { + return MmsStaticBrcbControlStatus::invalid_identity; + } + + tick(now_ms); + if (!reserved_) { + std::copy_n(client.owner.begin(), client.owner_size, owner_.begin()); + owner_size_ = client.owner_size; + association_id_ = client.association_id; + resv_tms_seconds_ = resv_tms_seconds; + reservation_expires_at_ms_ = 0U; + reserved_ = true; + owner_connected_ = true; + return MmsStaticBrcbControlStatus::ok; + } + + if (!same_owner(client)) { + return MmsStaticBrcbControlStatus::object_access_denied; + } + if (owner_connected_ && association_id_ != client.association_id) { + return MmsStaticBrcbControlStatus::object_access_denied; + } + + // A disconnected stable Owner may reclaim its reservation before timeout. + association_id_ = client.association_id; + owner_connected_ = true; + resv_tms_seconds_ = resv_tms_seconds; + reservation_expires_at_ms_ = 0U; + return MmsStaticBrcbControlStatus::ok; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::require_control_access( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + if (!valid()) { + return MmsStaticBrcbControlStatus::invalid_runtime; + } + if (!client.valid()) { + return MmsStaticBrcbControlStatus::invalid_identity; + } + tick(now_ms); + return owns_live_association(client) + ? MmsStaticBrcbControlStatus::ok + : MmsStaticBrcbControlStatus::object_access_denied; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::release( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + clear_reservation(); + return MmsStaticBrcbControlStatus::ok; +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::set_report_enabled( + const MmsStaticBrcbClientIdentity& client, + const bool enabled, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + return map_runtime_status(reports_->set_enabled(enabled)); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::replay_from( + const MmsStaticBrcbClientIdentity& client, + const std::span entry_id, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->replay_from(entry_id)); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::resume_after( + const MmsStaticBrcbClientIdentity& client, + const std::span entry_id, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->resume_after(entry_id)); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::rewind_to_oldest( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->rewind_to_oldest()); +} + +MmsStaticBrcbControlStatus MmsStaticBrcbControl::purge_buffer( + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now_ms) noexcept { + const auto access = require_control_access(client, now_ms); + if (access != MmsStaticBrcbControlStatus::ok) { + return access; + } + if (reports_->enabled()) { + return MmsStaticBrcbControlStatus::temporarily_unavailable; + } + return map_runtime_status(reports_->purge_buffer()); +} + +void MmsStaticBrcbControl::on_association_closed( + const std::uint64_t association_id, + const std::uint64_t now_ms) noexcept { + tick(now_ms); + if (!reserved_ || !owner_connected_ || association_id == 0U || + association_id != association_id_) { + return; + } + + if (reports_ != nullptr && reports_->valid() && reports_->enabled()) { + static_cast(reports_->set_enabled(false)); + } + association_id_ = 0U; + owner_connected_ = false; + if (resv_tms_seconds_ == 0U) { + clear_reservation(); + return; + } + reservation_expires_at_ms_ = reservation_deadline(now_ms, resv_tms_seconds_); +} + +MmsStaticBrcbControlStateView MmsStaticBrcbControl::state( + const std::uint64_t now_ms) noexcept { + tick(now_ms); + return { + reserved_, + owner_connected_, + reports_ != nullptr && reports_->valid() && reports_->enabled(), + resv_tms_seconds_, + association_id_, + reservation_expires_at_ms_, + {owner_.data(), owner_size_}}; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_brcb_objects.cpp b/src/mms/static_brcb_objects.cpp new file mode 100644 index 000000000..840f453a1 --- /dev/null +++ b/src/mms/static_brcb_objects.cpp @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_objects.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kUnsigned32Type{0x86U, 0x01U, 0x20U}; +constexpr std::array kInteger16Type{0x85U, 0x01U, 0x10U}; +constexpr std::array kOctetString8Type{0x89U, 0x01U, 0x08U}; +constexpr std::array kOctetString16Type{0x89U, 0x01U, 0x10U}; +constexpr std::array kVisible255Type{0x8AU, 0x02U, 0x00U, 0xFFU}; + +constexpr std::array + kAttributes{ + MmsStaticBrcbAttribute::report_id, + MmsStaticBrcbAttribute::report_enabled, + MmsStaticBrcbAttribute::data_set, + MmsStaticBrcbAttribute::conf_revision, + MmsStaticBrcbAttribute::purge_buffer, + MmsStaticBrcbAttribute::entry_id, + MmsStaticBrcbAttribute::reservation_time, + MmsStaticBrcbAttribute::owner}; + +constexpr std::array + kSuffixes{ + "RptID", + "RptEna", + "DatSet", + "ConfRev", + "PurgeBuf", + "EntryID", + "ResvTms", + "Owner"}; + +constexpr std::uint32_t kHardwareFault = 1U; +constexpr std::uint32_t kTemporarilyUnavailable = 2U; +constexpr std::uint32_t kObjectAccessDenied = 3U; +constexpr std::uint32_t kTypeInconsistent = 7U; +constexpr std::uint32_t kObjectValueInvalid = 11U; + +[[nodiscard]] std::span as_bytes( + const std::string_view text) noexcept { + return { + reinterpret_cast(text.data()), + text.size()}; +} + +[[nodiscard]] std::uint64_t now_ms(const MmsStaticBrcbObjectContext& context) noexcept { + return context.now_ms == nullptr + ? std::uint64_t{0U} + : context.now_ms(context.now_context); +} + +[[nodiscard]] std::span type_for( + const MmsStaticBrcbAttribute attribute) noexcept { + switch (attribute) { + case MmsStaticBrcbAttribute::report_id: + case MmsStaticBrcbAttribute::data_set: + return kVisible255Type; + case MmsStaticBrcbAttribute::report_enabled: + case MmsStaticBrcbAttribute::purge_buffer: + return kBooleanType; + case MmsStaticBrcbAttribute::conf_revision: + return kUnsigned32Type; + case MmsStaticBrcbAttribute::entry_id: + return kOctetString8Type; + case MmsStaticBrcbAttribute::reservation_time: + return kInteger16Type; + case MmsStaticBrcbAttribute::owner: + return kOctetString16Type; + } + return {}; +} + +[[nodiscard]] bool writable_attribute(const MmsStaticBrcbAttribute attribute) noexcept { + return attribute == MmsStaticBrcbAttribute::report_enabled || + attribute == MmsStaticBrcbAttribute::purge_buffer || + attribute == MmsStaticBrcbAttribute::entry_id || + attribute == MmsStaticBrcbAttribute::reservation_time; +} + +[[nodiscard]] wire::EncodeResult capacity_result( + const std::size_t required, + const std::span destination) noexcept { + return destination.size() < required + ? wire::EncodeResult{wire::EncodeStatus::buffer_too_small, 0U, required} + : wire::EncodeResult{wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] wire::EncodeResult encode_boolean( + const bool value, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + const auto capacity = capacity_result(required, destination); + if (!capacity.success()) { + return capacity; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return capacity; +} + +[[nodiscard]] std::size_t unsigned_content_size(std::uint32_t value) noexcept { + std::size_t size = 1U; + while (value > 0xFFU) { + ++size; + value >>= 8U; + } + return size; +} + +[[nodiscard]] wire::EncodeResult encode_unsigned( + const std::uint32_t value, + const std::span destination) noexcept { + const auto content_size = unsigned_content_size(value); + const auto total = asn1::BerSpanWriter::tlv_size(6, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 6, content_size)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + for (std::size_t index = content_size; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + } + return writer.size() == *total + ? capacity + : wire::EncodeResult{wire::EncodeStatus::value_out_of_range, 0U, *total}; +} + +[[nodiscard]] wire::EncodeResult encode_nonnegative_integer16( + const std::uint32_t value, + const std::span destination) noexcept { + if (value > 0x7FFFU) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const std::size_t content_size = value <= 0x7FU ? 1U : 2U; + const auto total = asn1::BerSpanWriter::tlv_size(5, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 5, content_size)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + if (content_size == 2U && + !writer.write_byte(static_cast((value >> 8U) & 0x7FU))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + if (!writer.write_byte(static_cast(value & 0xFFU)) || + writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_visible( + const std::string_view text, + const std::span destination) noexcept { + const auto total = asn1::BerSpanWriter::tlv_size(10, text.size()); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, text.size()) || + !writer.write_bytes(as_bytes(text)) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_data_set_reference( + const std::string_view domain, + const std::string_view item, + const std::span destination) noexcept { + if (domain.size() > std::numeric_limits::max() - 1U - item.size()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content_size = domain.size() + 1U + item.size(); + const auto total = asn1::BerSpanWriter::tlv_size(10, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, content_size) || + !writer.write_bytes(as_bytes(domain)) || + !writer.write_byte(static_cast('/')) || + !writer.write_bytes(as_bytes(item)) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_octets( + const std::span bytes, + const std::span destination) noexcept { + const auto total = asn1::BerSpanWriter::tlv_size(9, bytes.size()); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 9, bytes.size()) || + !writer.write_bytes(bytes) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult read_brcb_attribute( + const void* raw_context, + const std::span destination) noexcept { + const auto* context = static_cast(raw_context); + if (context == nullptr || context->definition == nullptr || + context->reports == nullptr || context->control == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + switch (context->attribute) { + case MmsStaticBrcbAttribute::report_id: + return encode_visible(context->definition->report_id, destination); + case MmsStaticBrcbAttribute::report_enabled: + return encode_boolean(context->reports->enabled(), destination); + case MmsStaticBrcbAttribute::data_set: + return encode_data_set_reference( + context->definition->data_set_domain, + context->definition->data_set_item, + destination); + case MmsStaticBrcbAttribute::conf_revision: + return encode_unsigned(context->definition->conf_revision, destination); + case MmsStaticBrcbAttribute::purge_buffer: + return encode_boolean(false, destination); + case MmsStaticBrcbAttribute::entry_id: { + const auto entry_id = context->reports->latest_entry_id(); + return encode_octets(entry_id, destination); + } + case MmsStaticBrcbAttribute::reservation_time: { + const auto state = context->control->state(now_ms(*context)); + return encode_nonnegative_integer16(state.resv_tms_seconds, destination); + } + case MmsStaticBrcbAttribute::owner: { + const auto state = context->control->state(now_ms(*context)); + return encode_octets(state.owner, destination); + } + } + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; +} + +[[nodiscard]] bool decode_boolean( + const std::span encoded, + bool& value) noexcept { + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 3 || tlv.constructed || tlv.value.size() != 1U) { + return false; + } + value = tlv.value[0] != 0U; + return true; +} + +[[nodiscard]] bool decode_nonnegative_integer16( + const std::span encoded, + std::uint32_t& value) noexcept { + value = 0U; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 5 || tlv.constructed) { + return false; + } + const auto decoded = asn1::BerSpanReader::read_signed_integer(tlv); + if (!decoded || *decoded < 0 || *decoded > 0x7FFF) { + return false; + } + value = static_cast(*decoded); + return true; +} + +[[nodiscard]] bool decode_entry_id( + const std::span encoded, + std::span& entry_id) noexcept { + entry_id = {}; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 9 || tlv.constructed || + tlv.value.size() != MmsInformationReportSpanCodec::entry_id_bytes) { + return false; + } + entry_id = tlv.value; + return true; +} + +[[nodiscard]] bool all_zero(const std::span bytes) noexcept { + return std::all_of(bytes.begin(), bytes.end(), [](const std::uint8_t byte) { + return byte == 0U; + }); +} + +[[nodiscard]] bool make_client( + const MmsStaticRequestAccessContext& access, + MmsStaticBrcbClientIdentity& client) noexcept { + client = {}; + if (!access.valid() || + access.owner.size() > MmsStaticBrcbClientIdentity::maximum_owner_bytes) { + return false; + } + client.association_id = access.association_id; + client.owner_size = access.owner.size(); + std::copy(access.owner.begin(), access.owner.end(), client.owner.begin()); + return client.valid(); +} + +[[nodiscard]] MmsStaticWriteResult map_control_status( + const MmsStaticBrcbControlStatus status) noexcept { + switch (status) { + case MmsStaticBrcbControlStatus::ok: + return {true, 0U}; + case MmsStaticBrcbControlStatus::object_access_denied: + case MmsStaticBrcbControlStatus::invalid_identity: + return {false, kObjectAccessDenied}; + case MmsStaticBrcbControlStatus::temporarily_unavailable: + return {false, kTemporarilyUnavailable}; + case MmsStaticBrcbControlStatus::entry_not_found: + return {false, kObjectValueInvalid}; + case MmsStaticBrcbControlStatus::invalid_runtime: + case MmsStaticBrcbControlStatus::backend_failure: + return {false, kHardwareFault}; + } + return {false, kHardwareFault}; +} + +[[nodiscard]] MmsStaticBrcbControlStatus ensure_claimed( + MmsStaticBrcbObjectContext& context, + const MmsStaticBrcbClientIdentity& client, + const std::uint64_t now) noexcept { + const auto state = context.control->state(now); + return state.reserved + ? MmsStaticBrcbControlStatus::ok + : context.control->reserve(client, 0U, now); +} + +[[nodiscard]] MmsStaticWriteResult write_brcb_attribute( + void* raw_context, + const std::span encoded_data, + const MmsStaticRequestAccessContext& access) noexcept { + auto* context = static_cast(raw_context); + if (context == nullptr || context->definition == nullptr || + context->reports == nullptr || context->control == nullptr) { + return {false, kHardwareFault}; + } + + MmsStaticBrcbClientIdentity client; + if (!make_client(access, client)) { + return {false, kObjectAccessDenied}; + } + const auto now = now_ms(*context); + + switch (context->attribute) { + case MmsStaticBrcbAttribute::report_enabled: { + bool value = false; + if (!decode_boolean(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + const auto state = context->control->state(now); + if (!value && !state.reserved) { + return {true, 0U}; + } + const auto claim = ensure_claimed(*context, client, now); + if (claim != MmsStaticBrcbControlStatus::ok) { + return map_control_status(claim); + } + return map_control_status( + context->control->set_report_enabled(client, value, now)); + } + case MmsStaticBrcbAttribute::purge_buffer: { + bool value = false; + if (!decode_boolean(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + if (!value) { + return {true, 0U}; + } + const auto claim = ensure_claimed(*context, client, now); + if (claim != MmsStaticBrcbControlStatus::ok) { + return map_control_status(claim); + } + return map_control_status(context->control->purge_buffer(client, now)); + } + case MmsStaticBrcbAttribute::entry_id: { + std::span entry_id; + if (!decode_entry_id(encoded_data, entry_id)) { + return {false, kTypeInconsistent}; + } + const auto claim = ensure_claimed(*context, client, now); + if (claim != MmsStaticBrcbControlStatus::ok) { + return map_control_status(claim); + } + return all_zero(entry_id) + ? map_control_status(context->control->rewind_to_oldest(client, now)) + : map_control_status(context->control->resume_after(client, entry_id, now)); + } + case MmsStaticBrcbAttribute::reservation_time: { + std::uint32_t seconds = 0U; + if (!decode_nonnegative_integer16(encoded_data, seconds)) { + return {false, kTypeInconsistent}; + } + const auto state = context->control->state(now); + if (seconds == 0U) { + return !state.reserved + ? MmsStaticWriteResult{true, 0U} + : map_control_status(context->control->release(client, now)); + } + return map_control_status(context->control->reserve(client, seconds, now)); + } + case MmsStaticBrcbAttribute::report_id: + case MmsStaticBrcbAttribute::data_set: + case MmsStaticBrcbAttribute::conf_revision: + case MmsStaticBrcbAttribute::owner: + return {false, kObjectAccessDenied}; + } + return {false, kHardwareFault}; +} + +} // namespace + +std::size_t MmsStaticBrcbObjectBank::required_object_capacity() const noexcept { + if (base_objects_.size() > + std::numeric_limits::max() - attributes_per_control_block) { + return std::numeric_limits::max(); + } + return base_objects_.size() + attributes_per_control_block; +} + +std::size_t MmsStaticBrcbObjectBank::required_name_bytes() const noexcept { + if (definition_ == nullptr) { + return std::numeric_limits::max(); + } + std::size_t total = 0U; + for (const auto suffix : kSuffixes) { + if (definition_->item.size() > + MmsServiceSpanCodec::maximum_identifier_bytes - 1U - suffix.size()) { + return std::numeric_limits::max(); + } + const auto bytes = definition_->item.size() + 1U + suffix.size(); + if (bytes > std::numeric_limits::max() - total) { + return std::numeric_limits::max(); + } + total += bytes; + } + return total; +} + +bool MmsStaticBrcbObjectBank::initialize() noexcept { + initialized_ = false; + object_count_ = 0U; + name_bytes_used_ = 0U; + table_ = MmsStaticObjectTable{std::span{}}; + + if (definition_ == nullptr || reports_ == nullptr || control_ == nullptr || + !reports_->valid() || !control_->valid()) { + return false; + } + const auto required_objects = required_object_capacity(); + const auto required_names = required_name_bytes(); + if (required_objects == std::numeric_limits::max() || + required_names == std::numeric_limits::max() || + required_objects > object_storage_.size() || + required_objects > MmsStaticObjectTable::maximum_objects || + attributes_per_control_block > context_storage_.size() || + required_names > name_storage_.size()) { + return false; + } + + std::copy(base_objects_.begin(), base_objects_.end(), object_storage_.begin()); + std::size_t object_offset = base_objects_.size(); + std::size_t name_offset = 0U; + + for (std::size_t attribute_index = 0U; + attribute_index < attributes_per_control_block; + ++attribute_index) { + const auto suffix = kSuffixes[attribute_index]; + const auto name_size = definition_->item.size() + 1U + suffix.size(); + auto* name = name_storage_.data() + name_offset; + std::copy(definition_->item.begin(), definition_->item.end(), name); + name[definition_->item.size()] = '$'; + std::copy( + suffix.begin(), suffix.end(), + name + definition_->item.size() + 1U); + + auto& context = context_storage_[attribute_index]; + context = MmsStaticBrcbObjectContext{ + definition_, + reports_, + control_, + kAttributes[attribute_index], + now_ms_, + now_context_}; + + const auto attribute = kAttributes[attribute_index]; + const auto writable = writable_attribute(attribute); + object_storage_[object_offset] = MmsStaticObjectEntry{ + definition_->domain, + std::string_view{name, name_size}, + type_for(attribute), + read_brcb_attribute, + &context, + false, + nullptr, + writable ? &context : nullptr, + writable ? write_brcb_attribute : nullptr}; + + name_offset += name_size; + ++object_offset; + } + + table_ = MmsStaticObjectTable{ + std::span{object_storage_.data(), object_offset}}; + if (!table_.valid()) { + table_ = MmsStaticObjectTable{std::span{}}; + return false; + } + + object_count_ = object_offset; + name_bytes_used_ = name_offset; + initialized_ = true; + return true; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_brcb_runtime.cpp b/src/mms/static_brcb_runtime.cpp new file mode 100644 index 000000000..509b5beab --- /dev/null +++ b/src/mms/static_brcb_runtime.cpp @@ -0,0 +1,561 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include "ariec61850/mms/buffered_selective_report_detail.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::uint8_t kTriggerDataChange = 0x40U; +constexpr std::uint8_t kTriggerQualityChange = 0x20U; +constexpr std::uint8_t kTriggerDataUpdate = 0x10U; +constexpr std::uint8_t kAllowedTriggers = 0x70U; + +constexpr std::uint8_t kReasonDataChange = 0x80U; +constexpr std::uint8_t kReasonQualityChange = 0x40U; +constexpr std::uint8_t kReasonDataUpdate = 0x20U; + +constexpr std::uint8_t kOptReasonForInclusion = 0x10U; +constexpr std::uint8_t kOptBufferOverflow = 0x02U; +constexpr std::uint8_t kOptEntryId = 0x01U; +constexpr std::uint8_t kAllowedOptionalFirst = 0x7FU; +constexpr std::uint8_t kAllowedOptionalSecond = 0x80U; + +[[nodiscard]] std::uint64_t saturating_add( + const std::uint64_t base, + const std::uint64_t delta) noexcept { + return delta > std::numeric_limits::max() - base + ? std::numeric_limits::max() + : base + delta; +} + +void bump_revision(std::uint32_t& revision) noexcept { + revision = revision == std::numeric_limits::max() + ? 1U + : revision + 1U; +} + +[[nodiscard]] std::uint8_t next_sequence_number( + const std::uint8_t current) noexcept { + return static_cast( + (static_cast(current) + 1U) & 0xFFU); +} + +[[nodiscard]] std::uint64_t next_entry_number( + const std::uint64_t current) noexcept { + return current == std::numeric_limits::max() + ? 1U + : current + 1U; +} + +[[nodiscard]] std::span as_bytes( + const std::string_view text) noexcept { + return { + reinterpret_cast(text.data()), + text.size()}; +} + +[[nodiscard]] MmsObjectNameView object_name( + const std::string_view domain, + const std::string_view item) noexcept { + return { + MmsObjectNameViewKind::domain_specific, + as_bytes(domain), + as_bytes(item)}; +} + +[[nodiscard]] bool visible_ascii( + const std::string_view text, + const std::size_t maximum_bytes) noexcept { + if (text.empty() || text.size() > maximum_bytes) { + return false; + } + for (const auto character : text) { + const auto byte = static_cast(character); + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] bool valid_reference( + const std::string_view domain, + const std::string_view item) noexcept { + return visible_ascii(domain, MmsInformationReportSpanCodec::maximum_reference_bytes) && + visible_ascii(item, MmsInformationReportSpanCodec::maximum_reference_bytes) && + domain.size() + 1U + item.size() <= + MmsInformationReportSpanCodec::maximum_reference_bytes; +} + +void clear_pending(MmsStaticBrcbPendingState& pending) noexcept { + std::fill( + pending.member_reason_masks.begin(), + pending.member_reason_masks.end(), + std::uint8_t{0U}); + pending.due_ms = 0U; + pending.pending_member_count = 0U; + pending.pending = false; +} + +[[nodiscard]] bool event_mapping( + const MmsStaticBrcbEventReason reason, + std::uint8_t& trigger_mask, + std::uint8_t& report_reason) noexcept { + switch (reason) { + case MmsStaticBrcbEventReason::data_change: + trigger_mask = kTriggerDataChange; + report_reason = kReasonDataChange; + return true; + case MmsStaticBrcbEventReason::quality_change: + trigger_mask = kTriggerQualityChange; + report_reason = kReasonQualityChange; + return true; + case MmsStaticBrcbEventReason::data_update: + trigger_mask = kTriggerDataUpdate; + report_reason = kReasonDataUpdate; + return true; + } + return false; +} + +[[nodiscard]] std::array +encode_entry_id(const std::uint64_t entry_number) noexcept { + std::array result{}; + for (std::size_t index = 0U; index < result.size(); ++index) { + const auto byte_index = result.size() - 1U - index; + const auto shift = static_cast(index * 8U); + result[byte_index] = static_cast( + (entry_number >> shift) & 0xFFU); + } + return result; +} + +[[nodiscard]] bool entry_id_equal( + const std::span left, + const std::array& right) + noexcept { + return left.size() == right.size() && + std::equal(left.begin(), left.end(), right.begin()); +} + +} // namespace + +bool MmsStaticBrcbRuntime::initialize() noexcept { + initialized_ = false; + if (definition_ == nullptr || pending_ == nullptr || objects_ == nullptr || + data_sets_ == nullptr || !objects_->valid() || !data_sets_->valid() || + !data_sets_->valid_against(*objects_) || slots_.empty()) { + return false; + } + if (!valid_reference(definition_->domain, definition_->item) || + !visible_ascii( + definition_->report_id, + MmsInformationReportSpanCodec::maximum_report_id_bytes) || + !valid_reference(definition_->data_set_domain, definition_->data_set_item) || + (definition_->optional_fields[0] & + static_cast(~kAllowedOptionalFirst)) != 0U || + (definition_->optional_fields[1] & + static_cast(~kAllowedOptionalSecond)) != 0U || + (definition_->optional_fields[0] & + static_cast(kOptBufferOverflow | kOptEntryId)) != + static_cast(kOptBufferOverflow | kOptEntryId) || + definition_->trigger_options == 0U || + (definition_->trigger_options & static_cast(~kAllowedTriggers)) != 0U) { + return false; + } + + const auto* data_set = data_sets_->find(object_name( + definition_->data_set_domain, definition_->data_set_item)); + if (data_set == nullptr || data_set->members.empty() || + data_set->members.size() > MmsInformationReportSpanCodec::maximum_members) { + return false; + } + for (const auto& slot : slots_) { + if (slot.storage.empty()) { + return false; + } + } + + *pending_ = {}; + pending_->revision = 1U; + clear_retained_slots(); + head_ = 0U; + count_ = 0U; + delivery_offset_ = 0U; + next_entry_number_ = 1U; + dropped_reports_ = 0U; + queue_revision_ = 1U; + sequence_number_ = 0U; + replay_gap_ = false; + enabled_ = false; + initialized_ = true; + return true; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::set_enabled( + const bool enabled) noexcept { + if (!initialized_ || pending_ == nullptr) { + return MmsStaticBrcbStatus::invalid_runtime; + } + if (enabled_ == enabled) { + return MmsStaticBrcbStatus::ok; + } + enabled_ = enabled; + clear_pending(*pending_); + bump_revision(pending_->revision); + return MmsStaticBrcbStatus::ok; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::notify( + const std::size_t data_set_member_index, + const MmsStaticBrcbEventReason reason, + const std::uint64_t now_ms) noexcept { + if (!initialized_ || definition_ == nullptr || pending_ == nullptr || + data_sets_ == nullptr) { + return MmsStaticBrcbStatus::invalid_runtime; + } + if (!enabled_) { + return MmsStaticBrcbStatus::temporarily_unavailable; + } + + std::uint8_t trigger_mask = 0U; + std::uint8_t report_reason = 0U; + if (!event_mapping(reason, trigger_mask, report_reason) || + (definition_->trigger_options & trigger_mask) == 0U) { + return MmsStaticBrcbStatus::trigger_not_selected; + } + + const auto* data_set = data_sets_->find(object_name( + definition_->data_set_domain, definition_->data_set_item)); + if (data_set == nullptr) { + return MmsStaticBrcbStatus::data_set_not_found; + } + if (data_set_member_index >= data_set->members.size() || + data_set_member_index >= MmsInformationReportSpanCodec::maximum_members) { + return MmsStaticBrcbStatus::member_out_of_range; + } + + if (!pending_->pending) { + pending_->pending = true; + pending_->due_ms = saturating_add(now_ms, definition_->buffer_time_ms); + } + if (pending_->member_reason_masks[data_set_member_index] == 0U) { + ++pending_->pending_member_count; + } + pending_->member_reason_masks[data_set_member_index] = static_cast( + pending_->member_reason_masks[data_set_member_index] | report_reason); + bump_revision(pending_->revision); + return MmsStaticBrcbStatus::ok; +} + +bool MmsStaticBrcbRuntime::next_due( + const std::uint64_t now_ms, + MmsStaticBrcbCapturePlan& plan) const noexcept { + plan = {}; + if (!initialized_ || pending_ == nullptr || !enabled_ || + !pending_->pending || pending_->pending_member_count == 0U || + now_ms < pending_->due_ms || slots_.empty()) { + return false; + } + plan.pending_revision = pending_->revision; + plan.queue_revision = queue_revision_; + plan.entry_number = next_entry_number_; + plan.sequence_number = next_sequence_number(sequence_number_); + plan.buffer_overflow = count_ == slots_.size() && delivery_offset_ == 0U; + return true; +} + +MmsStaticBrcbCaptureResult MmsStaticBrcbRuntime::capture( + const MmsStaticBrcbCapturePlan& plan, + const std::span report_time, + const std::span encode_buffer, + const std::span workspace) noexcept { + MmsStaticBrcbCaptureResult result; + if (!initialized_ || definition_ == nullptr || pending_ == nullptr || + objects_ == nullptr || data_sets_ == nullptr || slots_.empty() || + delivery_offset_ > count_) { + result.status = MmsStaticBrcbStatus::invalid_runtime; + return result; + } + const bool full = count_ == slots_.size(); + const bool overflow = full && delivery_offset_ == 0U; + if (!enabled_ || !pending_->pending || pending_->pending_member_count == 0U || + plan.pending_revision != pending_->revision || + plan.queue_revision != queue_revision_ || + plan.entry_number != next_entry_number_ || + plan.sequence_number != next_sequence_number(sequence_number_) || + plan.buffer_overflow != overflow) { + result.status = MmsStaticBrcbStatus::stale_plan; + return result; + } + + const auto* data_set = data_sets_->find(object_name( + definition_->data_set_domain, definition_->data_set_item)); + if (data_set == nullptr) { + result.status = MmsStaticBrcbStatus::data_set_not_found; + return result; + } + + std::array indices{}; + std::array references{}; + std::array results{}; + std::array reasons{}; + + std::size_t included = 0U; + std::size_t workspace_offset = 0U; + for (std::size_t member_index = 0U; + member_index < data_set->members.size(); + ++member_index) { + const auto reason_mask = pending_->member_reason_masks[member_index]; + if (reason_mask == 0U) { + continue; + } + const auto& member = data_set->members[member_index]; + const auto* object = objects_->find(object_name(member.domain, member.item)); + if (object == nullptr) { + result.status = MmsStaticBrcbStatus::object_not_found; + result.included_member_count = included; + return result; + } + const auto remaining = workspace.subspan(workspace_offset); + const auto read = object->read(object->context, remaining); + if (read.status == wire::EncodeStatus::buffer_too_small) { + result.status = MmsStaticBrcbStatus::workspace_too_small; + if (read.required_bytes <= + std::numeric_limits::max() - workspace_offset) { + result.required_bytes = workspace_offset + read.required_bytes; + } + result.included_member_count = included; + return result; + } + if (!read.success() || read.bytes_written > remaining.size() || + !detail::valid_mms_data(remaining.first(read.bytes_written))) { + result.status = MmsStaticBrcbStatus::backend_failure; + result.included_member_count = included; + return result; + } + + indices[included] = member_index; + references[included] = {member.domain, member.item}; + results[included] = {true, remaining.first(read.bytes_written), 0U}; + reasons[included] = reason_mask; + workspace_offset += read.bytes_written; + ++included; + } + if (included == 0U || included != pending_->pending_member_count) { + result.status = MmsStaticBrcbStatus::stale_plan; + result.included_member_count = included; + return result; + } + + const auto entry_id = encode_entry_id(plan.entry_number); + MmsBufferedSelectiveInformationReportSnapshotInput report; + report.report_id = definition_->report_id; + report.optional_fields = definition_->optional_fields; + report.sequence_number = plan.sequence_number; + report.report_time = report_time; + report.data_set_reference = { + definition_->data_set_domain, definition_->data_set_item}; + report.buffer_overflow = plan.buffer_overflow; + report.entry_id = entry_id; + report.conf_revision = definition_->conf_revision; + report.data_set_member_count = data_set->members.size(); + report.included_member_indices = + std::span{indices}.first(included); + report.included_member_references = + std::span{references}.first(included); + report.included_member_results = + std::span{results}.first(included); + if ((definition_->optional_fields[0] & kOptReasonForInclusion) != 0U) { + report.included_reason_for_inclusion = + std::span{reasons}.first(included); + } + + const auto encoded = + MmsBufferedSelectiveInformationReportSpanCodec::encode_snapshot_into( + report, encode_buffer); + if (!encoded.success()) { + result.status = encoded.status == wire::EncodeStatus::buffer_too_small + ? MmsStaticBrcbStatus::response_buffer_too_small + : MmsStaticBrcbStatus::report_encode_failed; + result.required_bytes = encoded.required_bytes; + result.included_member_count = included; + result.entry_id = entry_id; + return result; + } + + const auto target_index = full + ? head_ + : (head_ + count_) % slots_.size(); + auto& slot = slots_[target_index]; + if (slot.storage.size() < encoded.bytes_written) { + result.status = MmsStaticBrcbStatus::slot_too_small; + result.required_bytes = encoded.bytes_written; + result.included_member_count = included; + result.entry_id = entry_id; + return result; + } + + std::copy_n(encode_buffer.begin(), encoded.bytes_written, slot.storage.begin()); + slot.entry_id = entry_id; + slot.bytes = encoded.bytes_written; + slot.sequence_number = plan.sequence_number; + slot.buffer_overflow = plan.buffer_overflow; + slot.occupied = true; + + if (full) { + // The physical oldest slot is being recycled. If the delivery cursor is + // already past it, retain the same logical next-to-deliver entry by + // shifting the cursor with the head. Otherwise an undelivered report is + // irrecoverably lost and the recovery gap must remain visible. + if (delivery_offset_ == 0U) { + ++dropped_reports_; + replay_gap_ = true; + } else { + --delivery_offset_; + } + head_ = (head_ + 1U) % slots_.size(); + } else { + ++count_; + } + sequence_number_ = plan.sequence_number; + next_entry_number_ = next_entry_number(plan.entry_number); + clear_pending(*pending_); + bump_revision(pending_->revision); + bump_revision(queue_revision_); + + result.status = MmsStaticBrcbStatus::ok; + result.bytes_written = encoded.bytes_written; + result.required_bytes = encoded.required_bytes; + result.included_member_count = included; + result.entry_id = entry_id; + return result; +} + +bool MmsStaticBrcbRuntime::front(MmsStaticBrcbEntryView& entry) const noexcept { + entry = {}; + if (!initialized_ || count_ == 0U || delivery_offset_ >= count_ || + head_ >= slots_.size()) { + return false; + } + const auto physical = (head_ + delivery_offset_) % slots_.size(); + const auto& slot = slots_[physical]; + if (!slot.occupied || slot.bytes == 0U || slot.bytes > slot.storage.size()) { + return false; + } + entry.mms_pdu = std::span{slot.storage}.first(slot.bytes); + entry.entry_id = slot.entry_id; + entry.sequence_number = slot.sequence_number; + entry.buffer_overflow = slot.buffer_overflow; + return true; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::commit_delivery( + const std::span expected_entry_id) noexcept { + if (!initialized_ || slots_.empty() || delivery_offset_ > count_) { + return MmsStaticBrcbStatus::invalid_runtime; + } + if (delivery_offset_ >= count_ || head_ >= slots_.size()) { + return MmsStaticBrcbStatus::entry_not_found; + } + const auto physical = (head_ + delivery_offset_) % slots_.size(); + const auto& slot = slots_[physical]; + if (!slot.occupied || !entry_id_equal(expected_entry_id, slot.entry_id)) { + return MmsStaticBrcbStatus::entry_not_found; + } + + ++delivery_offset_; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +bool MmsStaticBrcbRuntime::find_entry( + const std::span entry_id, + std::size_t& logical_index) const noexcept { + logical_index = 0U; + if (!initialized_ || entry_id.size() != MmsInformationReportSpanCodec::entry_id_bytes || + count_ > slots_.size() || head_ >= slots_.size()) { + return false; + } + for (std::size_t logical = 0U; logical < count_; ++logical) { + const auto physical = (head_ + logical) % slots_.size(); + const auto& slot = slots_[physical]; + if (slot.occupied && entry_id_equal(entry_id, slot.entry_id)) { + logical_index = logical; + return true; + } + } + return false; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::replay_from( + const std::span entry_id) noexcept { + if (!initialized_) { + return MmsStaticBrcbStatus::invalid_runtime; + } + std::size_t logical{}; + if (!find_entry(entry_id, logical)) { + return MmsStaticBrcbStatus::entry_not_found; + } + delivery_offset_ = logical; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::resume_after( + const std::span entry_id) noexcept { + if (!initialized_) { + return MmsStaticBrcbStatus::invalid_runtime; + } + std::size_t logical{}; + if (!find_entry(entry_id, logical)) { + return MmsStaticBrcbStatus::entry_not_found; + } + delivery_offset_ = logical + 1U; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::rewind_to_oldest() noexcept { + if (!initialized_ || count_ > slots_.size()) { + return MmsStaticBrcbStatus::invalid_runtime; + } + delivery_offset_ = 0U; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +void MmsStaticBrcbRuntime::clear_retained_slots() noexcept { + for (auto& slot : slots_) { + const auto storage = slot.storage; + slot = {}; + slot.storage = storage; + } +} + +MmsStaticBrcbStatus MmsStaticBrcbRuntime::purge_buffer() noexcept { + if (!initialized_ || slots_.empty()) { + return MmsStaticBrcbStatus::invalid_runtime; + } + clear_retained_slots(); + head_ = 0U; + count_ = 0U; + delivery_offset_ = 0U; + replay_gap_ = false; + bump_revision(queue_revision_); + return MmsStaticBrcbStatus::ok; +} + +} // namespace ar::iec61850::mms \ No newline at end of file diff --git a/src/mms/static_brcb_state_codec.cpp b/src/mms/static_brcb_state_codec.cpp new file mode 100644 index 000000000..808b3f499 --- /dev/null +++ b/src/mms/static_brcb_state_codec.cpp @@ -0,0 +1,383 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_brcb_state_codec.hpp" + +#include "ariec61850/mms/static_brcb_runtime.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::array kMagicV1{ + 'A','R','B','R','C','B','Q','1'}; +constexpr std::array kMagicV2{ + 'A','R','B','R','C','B','Q','2'}; +constexpr std::size_t kHeaderBytesV1 = 48U; +constexpr std::size_t kHeaderBytesV2 = 56U; +constexpr std::size_t kEntryHeaderBytes = 16U; +constexpr std::uint8_t kReplayGapFlag = 0x01U; +constexpr std::uint64_t kFnvOffset = 14695981039346656037ULL; +constexpr std::uint64_t kFnvPrime = 1099511628211ULL; + +void write_u16( + const std::span destination, + const std::size_t offset, + const std::uint16_t value) noexcept { + destination[offset] = static_cast((value >> 8U) & 0xFFU); + destination[offset + 1U] = static_cast(value & 0xFFU); +} + +void write_u32( + const std::span destination, + const std::size_t offset, + const std::uint32_t value) noexcept { + for (std::size_t index = 0U; index < 4U; ++index) { + const auto shift = static_cast((3U - index) * 8U); + destination[offset + index] = static_cast((value >> shift) & 0xFFU); + } +} + +void write_u64( + const std::span destination, + const std::size_t offset, + const std::uint64_t value) noexcept { + for (std::size_t index = 0U; index < 8U; ++index) { + const auto shift = static_cast((7U - index) * 8U); + destination[offset + index] = static_cast((value >> shift) & 0xFFU); + } +} + +[[nodiscard]] std::uint16_t read_u16( + const std::span source, + const std::size_t offset) noexcept { + return static_cast( + (static_cast(source[offset]) << 8U) | + static_cast(source[offset + 1U])); +} + +[[nodiscard]] std::uint32_t read_u32( + const std::span source, + const std::size_t offset) noexcept { + std::uint32_t value = 0U; + for (std::size_t index = 0U; index < 4U; ++index) { + value = static_cast((value << 8U) | source[offset + index]); + } + return value; +} + +[[nodiscard]] std::uint64_t read_u64( + const std::span source, + const std::size_t offset) noexcept { + std::uint64_t value = 0U; + for (std::size_t index = 0U; index < 8U; ++index) { + value = (value << 8U) | source[offset + index]; + } + return value; +} + +[[nodiscard]] std::uint64_t next_entry_number(const std::uint64_t value) noexcept { + return value == std::numeric_limits::max() ? 1U : value + 1U; +} + +void hash_byte(std::uint64_t& hash, const std::uint8_t value) noexcept { + hash ^= value; + hash *= kFnvPrime; +} + +void hash_u64(std::uint64_t& hash, const std::uint64_t value) noexcept { + for (std::size_t index = 0U; index < 8U; ++index) { + const auto shift = static_cast((7U - index) * 8U); + hash_byte(hash, static_cast((value >> shift) & 0xFFU)); + } +} + +void hash_text(std::uint64_t& hash, const std::string_view text) noexcept { + hash_u64(hash, static_cast(text.size())); + for (const auto character : text) { + hash_byte(hash, static_cast(character)); + } +} + +[[nodiscard]] std::uint64_t definition_fingerprint( + const MmsStaticBrcbDefinition& definition) noexcept { + std::uint64_t hash = kFnvOffset; + hash_text(hash, definition.domain); + hash_text(hash, definition.item); + hash_text(hash, definition.report_id); + hash_text(hash, definition.data_set_domain); + hash_text(hash, definition.data_set_item); + hash_u64(hash, definition.conf_revision); + for (const auto field : definition.optional_fields) { + hash_byte(hash, field); + } + hash_u64(hash, definition.buffer_time_ms); + hash_byte(hash, definition.trigger_options); + return hash; +} + +[[nodiscard]] bool add_required( + std::size_t& required, + const std::size_t extra) noexcept { + if (extra > std::numeric_limits::max() - required) { + return false; + } + required += extra; + return true; +} + +[[nodiscard]] std::uint64_t decode_entry_id( + const std::span entry_id) noexcept { + std::uint64_t value = 0U; + for (const auto byte : entry_id) { + value = (value << 8U) | byte; + } + return value; +} + +void clear_slots(const std::span slots) noexcept { + for (auto& slot : slots) { + const auto storage = slot.storage; + slot = {}; + slot.storage = storage; + } +} + +[[nodiscard]] bool magic_matches( + const std::span source, + const std::span magic) noexcept { + return source.size() >= magic.size() && + std::equal(magic.begin(), magic.end(), source.begin()); +} + +} // namespace + +MmsStaticBrcbStateResult MmsStaticBrcbStateCodec::encode( + const MmsStaticBrcbRuntime& runtime, + const std::span destination) noexcept { + MmsStaticBrcbStateResult result; + if (!runtime.initialized_ || runtime.definition_ == nullptr || + runtime.pending_ == nullptr || runtime.slots_.empty() || + runtime.head_ >= runtime.slots_.size() || + runtime.count_ > runtime.slots_.size() || + runtime.delivery_offset_ > runtime.count_ || + runtime.count_ > std::numeric_limits::max() || + runtime.delivery_offset_ > std::numeric_limits::max()) { + result.status = MmsStaticBrcbStateStatus::invalid_runtime; + return result; + } + + std::size_t required = kHeaderBytesV2; + for (std::size_t logical = 0U; logical < runtime.count_; ++logical) { + const auto physical = (runtime.head_ + logical) % runtime.slots_.size(); + const auto& slot = runtime.slots_[physical]; + if (!slot.occupied || slot.bytes == 0U || slot.bytes > slot.storage.size() || + slot.bytes > std::numeric_limits::max() || + !add_required(required, kEntryHeaderBytes) || + !add_required(required, slot.bytes)) { + result.status = MmsStaticBrcbStateStatus::invalid_runtime; + return result; + } + } + result.required_bytes = required; + if (destination.size() < required) { + result.status = MmsStaticBrcbStateStatus::buffer_too_small; + return result; + } + + auto output = destination.first(required); + std::fill(output.begin(), output.end(), std::uint8_t{0U}); + std::copy(kMagicV2.begin(), kMagicV2.end(), output.begin()); + write_u16(output, 8U, format_version); + write_u16(output, 10U, static_cast(kHeaderBytesV2)); + write_u64(output, 12U, definition_fingerprint(*runtime.definition_)); + write_u32(output, 20U, static_cast(runtime.count_)); + write_u64(output, 24U, runtime.next_entry_number_); + write_u64(output, 32U, runtime.dropped_reports_); + output[40U] = runtime.sequence_number_; + output[41U] = runtime.replay_gap_ ? kReplayGapFlag : 0U; + write_u32(output, 44U, static_cast(runtime.delivery_offset_)); + + std::size_t offset = kHeaderBytesV2; + for (std::size_t logical = 0U; logical < runtime.count_; ++logical) { + const auto physical = (runtime.head_ + logical) % runtime.slots_.size(); + const auto& slot = runtime.slots_[physical]; + write_u32(output, offset, static_cast(slot.bytes)); + std::copy( + slot.entry_id.begin(), + slot.entry_id.end(), + output.subspan(offset + 4U).begin()); + output[offset + 12U] = slot.sequence_number; + output[offset + 13U] = slot.buffer_overflow ? 1U : 0U; + offset += kEntryHeaderBytes; + std::copy_n( + slot.storage.begin(), + slot.bytes, + output.subspan(offset).begin()); + offset += slot.bytes; + } + + result.status = MmsStaticBrcbStateStatus::ok; + result.bytes_written = required; + return result; +} + +MmsStaticBrcbStateResult MmsStaticBrcbStateCodec::restore( + MmsStaticBrcbRuntime& runtime, + const std::span source) noexcept { + MmsStaticBrcbStateResult result; + result.required_bytes = source.size(); + if (!runtime.initialized_ || runtime.definition_ == nullptr || + runtime.pending_ == nullptr || runtime.slots_.empty()) { + result.status = MmsStaticBrcbStateStatus::invalid_runtime; + return result; + } + + const bool is_v2 = source.size() >= kHeaderBytesV2 && + magic_matches(source, kMagicV2) && + read_u16(source, 8U) == format_version && + read_u16(source, 10U) == static_cast(kHeaderBytesV2); + const bool is_v1 = source.size() >= kHeaderBytesV1 && + magic_matches(source, kMagicV1) && + read_u16(source, 8U) == legacy_format_version && + read_u16(source, 10U) == static_cast(kHeaderBytesV1); + if (!is_v2 && !is_v1) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + + const auto header_bytes = is_v2 ? kHeaderBytesV2 : kHeaderBytesV1; + std::size_t delivery_offset = 0U; + bool replay_gap = false; + if (is_v2) { + if ((source[41U] & static_cast(~kReplayGapFlag)) != 0U || + source[42U] != 0U || source[43U] != 0U) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + for (std::size_t index = 48U; index < kHeaderBytesV2; ++index) { + if (source[index] != 0U) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + } + replay_gap = (source[41U] & kReplayGapFlag) != 0U; + delivery_offset = static_cast(read_u32(source, 44U)); + } else { + for (std::size_t index = 41U; index < kHeaderBytesV1; ++index) { + if (source[index] != 0U) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + } + } + + if (read_u64(source, 12U) != definition_fingerprint(*runtime.definition_)) { + result.status = MmsStaticBrcbStateStatus::definition_mismatch; + return result; + } + + const auto count = static_cast(read_u32(source, 20U)); + const auto restored_next_entry = read_u64(source, 24U); + const auto restored_dropped = read_u64(source, 32U); + const auto restored_sequence = source[40U]; + if (count > runtime.slots_.size()) { + result.status = MmsStaticBrcbStateStatus::capacity_mismatch; + return result; + } + if (delivery_offset > count || restored_next_entry == 0U) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + + std::size_t offset = header_bytes; + std::uint64_t previous_entry_number = 0U; + std::uint8_t last_sequence = 0U; + for (std::size_t logical = 0U; logical < count; ++logical) { + if (offset > source.size() || source.size() - offset < kEntryHeaderBytes) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + const auto pdu_bytes = static_cast(read_u32(source, offset)); + const auto entry_id = source.subspan(offset + 4U, 8U); + const auto sequence = source[offset + 12U]; + const auto overflow = source[offset + 13U]; + if (pdu_bytes == 0U || overflow > 1U || source[offset + 14U] != 0U || + source[offset + 15U] != 0U || + pdu_bytes > runtime.slots_[logical].storage.size()) { + result.status = pdu_bytes > runtime.slots_[logical].storage.size() + ? MmsStaticBrcbStateStatus::capacity_mismatch + : MmsStaticBrcbStateStatus::invalid_state; + return result; + } + offset += kEntryHeaderBytes; + if (offset > source.size() || pdu_bytes > source.size() - offset) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + + const auto entry_number = decode_entry_id(entry_id); + if (entry_number == 0U || + (logical != 0U && entry_number != next_entry_number(previous_entry_number))) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + previous_entry_number = entry_number; + last_sequence = sequence; + offset += pdu_bytes; + } + if (offset != source.size() || + (count != 0U && restored_next_entry != next_entry_number(previous_entry_number)) || + (count != 0U && restored_sequence != last_sequence)) { + result.status = MmsStaticBrcbStateStatus::invalid_state; + return result; + } + + clear_slots(runtime.slots_); + offset = header_bytes; + for (std::size_t logical = 0U; logical < count; ++logical) { + auto& slot = runtime.slots_[logical]; + const auto pdu_bytes = static_cast(read_u32(source, offset)); + std::copy_n( + source.subspan(offset + 4U).begin(), + slot.entry_id.size(), + slot.entry_id.begin()); + slot.sequence_number = source[offset + 12U]; + slot.buffer_overflow = source[offset + 13U] != 0U; + slot.bytes = pdu_bytes; + slot.occupied = true; + offset += kEntryHeaderBytes; + std::copy_n( + source.subspan(offset).begin(), + pdu_bytes, + slot.storage.begin()); + offset += pdu_bytes; + } + + runtime.head_ = 0U; + runtime.count_ = count; + runtime.delivery_offset_ = delivery_offset; + runtime.next_entry_number_ = restored_next_entry; + runtime.dropped_reports_ = restored_dropped; + runtime.sequence_number_ = restored_sequence; + runtime.replay_gap_ = replay_gap; + runtime.queue_revision_ = runtime.queue_revision_ == + std::numeric_limits::max() + ? 1U + : runtime.queue_revision_ + 1U; + *runtime.pending_ = {}; + runtime.pending_->revision = 1U; + runtime.enabled_ = false; + + result.status = MmsStaticBrcbStateStatus::ok; + result.bytes_written = source.size(); + return result; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_data_set_table.cpp b/src/mms/static_data_set_table.cpp new file mode 100644 index 000000000..3fa764748 --- /dev/null +++ b/src/mms/static_data_set_table.cpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_data_set_table.hpp" + +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool valid_name(const std::string_view value) noexcept { + if (value.empty() || value.size() > MmsServiceSpanCodec::maximum_identifier_bytes) { + return false; + } + for (const char ch : value) { + const auto byte = static_cast(ch); + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] bool equals( + const std::span bytes, + const std::string_view text) noexcept { + if (bytes.size() != text.size()) { + return false; + } + for (std::size_t index = 0U; index < bytes.size(); ++index) { + if (bytes[index] != static_cast( + static_cast(text[index]))) { + return false; + } + } + return true; +} + +[[nodiscard]] bool same_member( + const MmsStaticDataSetMember& left, + const MmsStaticDataSetMember& right) noexcept { + return left.domain == right.domain && left.item == right.item; +} + +} // namespace + +bool MmsStaticDataSetTable::valid() const noexcept { + if (data_sets_.size() > maximum_data_sets) { + return false; + } + for (std::size_t index = 0U; index < data_sets_.size(); ++index) { + const auto& data_set = data_sets_[index]; + if (!valid_name(data_set.domain) || !valid_name(data_set.item) || + data_set.members.empty() || + data_set.members.size() > MmsDataSetSpanCodec::maximum_members) { + return false; + } + for (std::size_t earlier = 0U; earlier < index; ++earlier) { + if (data_sets_[earlier].domain == data_set.domain && + data_sets_[earlier].item == data_set.item) { + return false; + } + } + for (std::size_t member = 0U; member < data_set.members.size(); ++member) { + const auto& current = data_set.members[member]; + if (!valid_name(current.domain) || !valid_name(current.item)) { + return false; + } + for (std::size_t earlier = 0U; earlier < member; ++earlier) { + if (same_member(data_set.members[earlier], current)) { + return false; + } + } + } + } + return true; +} + +bool MmsStaticDataSetTable::valid_against( + const MmsStaticObjectTable& objects) const noexcept { + if (!valid() || !objects.valid()) { + return false; + } + for (const auto& data_set : data_sets_) { + for (const auto& member : data_set.members) { + const MmsObjectNameView name{ + MmsObjectNameViewKind::domain_specific, + std::span{ + reinterpret_cast(member.domain.data()), + member.domain.size()}, + std::span{ + reinterpret_cast(member.item.data()), + member.item.size()}}; + if (objects.find(name) == nullptr) { + return false; + } + } + } + return true; +} + +const MmsStaticDataSetEntry* MmsStaticDataSetTable::find( + const MmsObjectNameView& name) const noexcept { + if (name.kind != MmsObjectNameViewKind::domain_specific) { + return nullptr; + } + for (const auto& data_set : data_sets_) { + if (equals(name.domain, data_set.domain) && equals(name.item, data_set.item)) { + return &data_set; + } + } + return nullptr; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_direct_control.cpp b/src/mms/static_direct_control.cpp new file mode 100644 index 000000000..c14d958a3 --- /dev/null +++ b/src/mms/static_direct_control.cpp @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_direct_control.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool context_tag( + const asn1::BerTlvView& tlv, + const std::int32_t tag, + const bool constructed) noexcept { + return tlv.tag_class == asn1::BerClass::context_specific && + tlv.tag_number == tag && tlv.constructed == constructed; +} + +[[nodiscard]] bool decode_boolean( + const asn1::BerTlvView& tlv, + bool& value) noexcept { + if (!context_tag(tlv, 3, false) || tlv.value.size() != 1U) { + return false; + } + value = tlv.value.front() != 0U; + return true; +} + +[[nodiscard]] bool read_next( + const std::span source, + std::size_t& offset, + asn1::BerTlvView& tlv) noexcept { + return asn1::BerSpanReader::try_read_tlv(source, offset, tlv); +} + +[[nodiscard]] bool decode_origin( + const asn1::BerTlvView& origin, + std::uint8_t& category) noexcept { + if (!context_tag(origin, 2, true)) { + return false; + } + std::size_t offset = 0U; + asn1::BerTlvView or_cat; + asn1::BerTlvView or_ident; + if (!read_next(origin.value, offset, or_cat) || + !context_tag(or_cat, 6, false)) { + return false; + } + const auto category_value = asn1::BerSpanReader::read_unsigned_integer(or_cat); + if (!category_value || *category_value > 0xFFU) { + return false; + } + if (!read_next(origin.value, offset, or_ident) || + !context_tag(or_ident, 9, false) || + or_ident.value.size() > 64U || + offset != origin.value.size()) { + return false; + } + category = static_cast(*category_value); + return true; +} + +[[nodiscard]] bool decode_check( + const asn1::BerTlvView& check, + bool& synchro, + bool& interlock) noexcept { + if (!context_tag(check, 4, false) || check.value.size() != 2U || + check.value[0] != 6U || (check.value[1] & 0x3FU) != 0U) { + return false; + } + synchro = (check.value[1] & 0x80U) != 0U; + interlock = (check.value[1] & 0x40U) != 0U; + return true; +} + +} // namespace + +bool try_decode_static_direct_boolean_operate( + const std::span encoded_data, + MmsStaticDirectBooleanOperate& decoded) noexcept { + decoded = {}; + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(encoded_data, outer) || + !context_tag(outer, 2, true)) { + return false; + } + + std::size_t offset = 0U; + asn1::BerTlvView ctl_val; + asn1::BerTlvView origin; + asn1::BerTlvView ctl_num; + asn1::BerTlvView timestamp; + asn1::BerTlvView test; + asn1::BerTlvView check; + + if (!read_next(outer.value, offset, ctl_val) || + !decode_boolean(ctl_val, decoded.control_value) || + !read_next(outer.value, offset, origin) || + !decode_origin(origin, decoded.origin_category) || + !read_next(outer.value, offset, ctl_num) || + !context_tag(ctl_num, 6, false)) { + return false; + } + const auto control_number = asn1::BerSpanReader::read_unsigned_integer(ctl_num); + if (!control_number || *control_number > 0xFFU) { + return false; + } + decoded.control_number = static_cast(*control_number); + + if (!read_next(outer.value, offset, timestamp) || + !context_tag(timestamp, 17, false) || timestamp.value.size() != 8U || + !read_next(outer.value, offset, test) || + !decode_boolean(test, decoded.test) || + !read_next(outer.value, offset, check) || + !decode_check(check, decoded.synchro_check, decoded.interlock_check) || + offset != outer.value.size()) { + return false; + } + return true; +} + +wire::EncodeResult mms_static_direct_boolean_read_state( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto* state = static_cast(context); + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = state->value != 0U ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +wire::EncodeResult mms_static_direct_normal_read_ctl_model( + const void*, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x86U; // MMS unsigned + destination[1] = 0x01U; + destination[2] = 0x01U; // ctlModel = Direct-with-normal-security + return {wire::EncodeStatus::ok, required, required}; +} + +wire::EncodeResult mms_static_control_read_unavailable( + const void*, + const std::span) noexcept { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; +} + +MmsStaticWriteResult mms_static_direct_boolean_write_oper( + void* context, + const std::span encoded_data) noexcept { + auto* binding = static_cast(context); + if (binding == nullptr || binding->state == nullptr) { + return {false, 10U}; + } + + MmsStaticDirectBooleanOperate operate; + if (!try_decode_static_direct_boolean_operate(encoded_data, operate)) { + ++binding->state->rejected_operations; + return {false, binding->policy.malformed_failure_code}; + } + if (operate.origin_category > 8U || operate.control_number == 0U || + (operate.test && !binding->policy.allow_test) || + (operate.synchro_check && !binding->policy.allow_synchro_check) || + (operate.interlock_check && !binding->policy.allow_interlock_check)) { + ++binding->state->rejected_operations; + return {false, binding->policy.invalid_value_failure_code}; + } + + if (!operate.test && binding->apply != nullptr && + !binding->apply(binding->apply_context, operate.control_value)) { + ++binding->state->rejected_operations; + return {false, binding->policy.backend_failure_code}; + } + + if (!operate.test) { + binding->state->value = operate.control_value ? 1U : 0U; + } + binding->state->last_control_number = operate.control_number; + binding->state->last_test = operate.test; + ++binding->state->accepted_operations; + return {true, 0U}; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_dispatcher.cpp b/src/mms/static_dispatcher.cpp new file mode 100644 index 000000000..63d5a3c1f --- /dev/null +++ b/src/mms/static_dispatcher.cpp @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_dispatcher.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool span_equals( + const std::span bytes, + const std::string_view text) noexcept { + if (bytes.size() != text.size()) { + return false; + } + for (std::size_t index = 0U; index < bytes.size(); ++index) { + if (bytes[index] != static_cast( + static_cast(text[index]))) { + return false; + } + } + return true; +} + +[[nodiscard]] bool valid_mms_data( + const std::span encoded) noexcept { + asn1::BerTlvView data; + if (!asn1::BerSpanReader::try_read_exact(encoded, data) || + data.tag_class != asn1::BerClass::context_specific) { + return false; + } + if (data.tag_number == 1 || data.tag_number == 2) { + return data.constructed; + } + return data.tag_number >= 3 && data.tag_number <= 17 && !data.constructed; +} + +[[nodiscard]] MmsStaticDispatchResult make_status( + const MmsStaticDispatchStatus status, + const MmsConfirmedPduView& request, + const std::size_t required_bytes = 0U) noexcept { + return MmsStaticDispatchResult{ + status, + request.service(), + request.invoke_id, + 0U, + required_bytes}; +} + +[[nodiscard]] MmsStaticDispatchResult make_encoded( + const MmsConfirmedPduView& request, + const wire::EncodeResult encoded) noexcept { + if (encoded.success()) { + return MmsStaticDispatchResult{ + MmsStaticDispatchStatus::response_ready, + request.service(), + request.invoke_id, + encoded.bytes_written, + encoded.required_bytes}; + } + if (encoded.status == wire::EncodeStatus::buffer_too_small) { + return make_status( + MmsStaticDispatchStatus::response_buffer_too_small, + request, + encoded.required_bytes); + } + return make_status(MmsStaticDispatchStatus::backend_failure, request); +} + +[[nodiscard]] bool policy_valid(const MmsStaticDispatchPolicy& policy) noexcept { + return policy.maximum_names_per_response > 0U && + policy.maximum_names_per_response <= MmsServiceSpanCodec::maximum_identifiers && + policy.maximum_write_variables > 0U && + policy.maximum_write_variables <= MmsServiceSpanCodec::maximum_variables; +} + +[[nodiscard]] bool append_unique( + std::array& names, + std::size_t& count, + const std::string_view value) noexcept { + for (std::size_t index = 0U; index < count; ++index) { + if (names[index] == value) { + return true; + } + } + if (count >= names.size()) { + return false; + } + names[count++] = value; + return true; +} + +// IEC 61850 engineering clients discover each Logical Node as one MMS +// NamedVariable and then walk its hierarchical TypeSpecification. Static +// profiles also keep flattened leaf aliases in the table so Read/Write can +// resolve exact FC/DO/DA paths. Do not advertise those aliases as additional +// top-level NamedVariables when their root Logical Node object is present. +// +// The fallback is intentional: a generic MMS profile that only supplies flat +// names (and no corresponding root entry) keeps the legacy directory behavior. +[[nodiscard]] bool is_flattened_child_with_root( + const MmsStaticObjectTable& objects, + const std::string_view domain, + const std::string_view item) noexcept { + const auto separator = item.find('$'); + if (separator == std::string_view::npos || separator == 0U) { + return false; + } + const auto root = item.substr(0U, separator); + for (const auto& candidate : objects.objects()) { + if (candidate.domain == domain && candidate.item == root) { + return true; + } + } + return false; +} + +[[nodiscard]] std::size_t collect_names( + const MmsStaticObjectTable& objects, + const MmsStaticDataSetTable& data_sets, + const MmsStaticDispatchPolicy& policy, + const MmsGetNameListRequestView& request, + std::array& names) noexcept { + std::size_t count = 0U; + if (request.object_class == MmsNameListObjectClass::domain && + request.scope == MmsNameScopeKind::vmd_specific) { + for (const auto& object : objects.objects()) { + if (!append_unique(names, count, object.domain)) { + return names.size() + 1U; + } + } + for (const auto& data_set : data_sets.data_sets()) { + if (!append_unique(names, count, data_set.domain)) { + return names.size() + 1U; + } + } + return count; + } + + if (request.object_class == MmsNameListObjectClass::named_variable && + request.scope == MmsNameScopeKind::domain_specific) { + for (const auto& object : objects.objects()) { + if (!span_equals(request.domain_id, object.domain) || + (!policy.advertise_flattened_child_aliases && + is_flattened_child_with_root(objects, object.domain, object.item))) { + continue; + } + if (!append_unique(names, count, object.item)) { + return names.size() + 1U; + } + } + return count; + } + + if (request.object_class == MmsNameListObjectClass::named_variable_list && + request.scope == MmsNameScopeKind::domain_specific) { + for (const auto& data_set : data_sets.data_sets()) { + if (span_equals(request.domain_id, data_set.domain)) { + if (count >= names.size()) { + return names.size() + 1U; + } + names[count++] = data_set.item; + } + } + return count; + } + + return names.size() + 1U; +} + +[[nodiscard]] MmsStaticDispatchResult dispatch_get_name_list( + const MmsStaticObjectTable& objects, + const MmsStaticDataSetTable& data_sets, + const MmsStaticDispatchPolicy& policy, + const MmsConfirmedPduView& confirmed, + const std::span response) noexcept { + MmsGetNameListRequestView request; + if (!MmsServiceSpanCodec::try_decode_get_name_list_request(confirmed, request)) { + return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); + } + + std::array names{}; + const auto name_count = collect_names(objects, data_sets, policy, request, names); + if (name_count > names.size()) { + return make_status(MmsStaticDispatchStatus::unsupported_request, confirmed); + } + + std::size_t start = 0U; + if (!request.continue_after.empty()) { + bool found = false; + for (std::size_t index = 0U; index < name_count; ++index) { + if (span_equals(request.continue_after, names[index])) { + start = index + 1U; + found = true; + break; + } + } + if (!found) { + return make_status(MmsStaticDispatchStatus::object_not_found, confirmed); + } + } + + const auto available = name_count - start; + auto page_count = std::min(available, policy.maximum_names_per_response); + if (available == 0U) { + const std::span empty; + return make_encoded( + confirmed, + MmsServiceSpanCodec::encode_get_name_list_response_into( + confirmed.invoke_id, empty, false, response)); + } + + while (page_count > 0U) { + const auto more_follows = start + page_count < name_count; + const auto encoded = MmsServiceSpanCodec::encode_get_name_list_response_into( + confirmed.invoke_id, + std::span{names}.subspan(start, page_count), + more_follows, + response); + if (encoded.success()) { + return make_encoded(confirmed, encoded); + } + if (encoded.status != wire::EncodeStatus::buffer_too_small || page_count == 1U) { + return make_encoded(confirmed, encoded); + } + --page_count; + } + return make_status(MmsStaticDispatchStatus::backend_failure, confirmed); +} + +[[nodiscard]] MmsStaticDispatchResult dispatch_attributes( + const MmsStaticObjectTable& objects, + const MmsConfirmedPduView& confirmed, + const std::span response) noexcept { + MmsVariableAccessAttributesRequestView request; + if (!MmsServiceSpanCodec::try_decode_variable_access_attributes_request( + confirmed, request)) { + return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); + } + const auto* object = objects.find(request.name); + if (object == nullptr) { + return make_status(MmsStaticDispatchStatus::object_not_found, confirmed); + } + return make_encoded( + confirmed, + MmsServiceSpanCodec::encode_variable_access_attributes_response_into( + confirmed.invoke_id, + object->mms_deletable, + object->type_specification, + response)); +} + +[[nodiscard]] MmsStaticDispatchResult dispatch_data_set_attributes( + const MmsStaticDataSetTable& data_sets, + const MmsConfirmedPduView& confirmed, + const std::span response) noexcept { + MmsNamedVariableListAttributesRequestView request; + if (!MmsDataSetSpanCodec::try_decode_get_named_variable_list_attributes_request( + confirmed, request)) { + return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); + } + const auto* data_set = data_sets.find(request.name); + if (data_set == nullptr) { + return make_status(MmsStaticDispatchStatus::object_not_found, confirmed); + } + + std::array members{}; + for (std::size_t index = 0U; index < data_set->members.size(); ++index) { + members[index] = MmsNamedVariableListMemberInput{ + data_set->members[index].domain, + data_set->members[index].item}; + } + return make_encoded( + confirmed, + MmsDataSetSpanCodec::encode_get_named_variable_list_attributes_response_into( + confirmed.invoke_id, + data_set->mms_deletable, + std::span{members}.first( + data_set->members.size()), + response)); +} + +[[nodiscard]] MmsStaticDispatchResult dispatch_read( + const MmsStaticObjectTable& objects, + const MmsStaticDispatchPolicy& policy, + const MmsConfirmedPduView& confirmed, + const std::span response, + const std::span workspace) noexcept { + MmsReadRequestView request; + if (!MmsServiceSpanCodec::try_decode_read_request(confirmed, request)) { + return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); + } + if (request.specification_with_result) { + // This bounded profile does not encode variableAccessSpecification in + // Read responses. Reject the optional request form explicitly before + // invoking any object callbacks instead of returning an incomplete PDU. + return make_status(MmsStaticDispatchStatus::unsupported_request, confirmed); + } + + std::array results{}; + std::size_t workspace_offset = 0U; + for (std::size_t index = 0U; index < request.variable_count; ++index) { + MmsObjectNameView name; + if (!request.try_variable(index, name)) { + return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); + } + const auto* object = objects.find(name); + if (object == nullptr) { + results[index] = MmsReadAccessResultInput{ + false, {}, policy.missing_object_failure_code}; + continue; + } + + const auto remaining = workspace.subspan(workspace_offset); + const auto read = object->read(object->context, remaining); + if (read.status == wire::EncodeStatus::buffer_too_small) { + return make_status( + MmsStaticDispatchStatus::workspace_too_small, + confirmed, + workspace_offset + read.required_bytes); + } + if (!read.success()) { + results[index] = MmsReadAccessResultInput{ + false, {}, policy.backend_failure_code}; + continue; + } + if (read.bytes_written > remaining.size() || + !valid_mms_data(remaining.first(read.bytes_written))) { + return make_status(MmsStaticDispatchStatus::backend_failure, confirmed); + } + results[index] = MmsReadAccessResultInput{ + true, + remaining.first(read.bytes_written), + 0U}; + workspace_offset += read.bytes_written; + } + + return make_encoded( + confirmed, + MmsServiceSpanCodec::encode_read_response_into( + confirmed.invoke_id, + std::span{results}.first(request.variable_count), + response)); +} + +[[nodiscard]] MmsStaticDispatchResult dispatch_write( + const MmsStaticObjectTable& objects, + const MmsStaticDispatchPolicy& policy, + const MmsConfirmedPduView& confirmed, + const std::span response, + const MmsStaticRequestAccessContext& access) noexcept { + MmsWriteRequestView request; + if (!MmsServiceSpanCodec::try_decode_write_request(confirmed, request)) { + return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); + } + if (request.variable_count > policy.maximum_write_variables) { + return make_status(MmsStaticDispatchStatus::unsupported_request, confirmed); + } + + std::array resolved{}; + std::array, MmsServiceSpanCodec::maximum_variables> values{}; + for (std::size_t index = 0U; index < request.variable_count; ++index) { + MmsObjectNameView name; + if (!request.try_variable(index, name) || !request.try_value(index, values[index])) { + return make_status(MmsStaticDispatchStatus::malformed_request, confirmed); + } + resolved[index] = objects.find(name); + } + + std::array results{}; + for (std::size_t index = 0U; index < request.variable_count; ++index) { + const auto* object = resolved[index]; + if (object == nullptr) { + results[index] = MmsWriteAccessResultInput{ + false, policy.missing_object_failure_code}; + continue; + } + if (!object->writable()) { + results[index] = MmsWriteAccessResultInput{ + false, policy.access_denied_failure_code}; + continue; + } + const auto applied = object->contextual_write != nullptr + ? object->contextual_write(object->write_context, values[index], access) + : object->write(object->write_context, values[index]); + results[index] = MmsWriteAccessResultInput{ + applied.success, + applied.success ? 0U : applied.failure_code}; + } + + return make_encoded( + confirmed, + MmsServiceSpanCodec::encode_write_response_into( + confirmed.invoke_id, + std::span{results}.first(request.variable_count), + response)); +} + +} // namespace + +MmsStaticDispatchResult MmsStaticApplicationDispatcher::dispatch( + const std::span mms_request, + const std::span response, + const std::span workspace, + const MmsStaticRequestAccessContext& access) const noexcept { + MmsConfirmedPduView request; + if (!MmsPduSpanCodec::try_decode_confirmed_request_view(mms_request, request)) { + return MmsStaticDispatchResult{ + MmsStaticDispatchStatus::malformed_request, + MmsWireConfirmedService::unknown, + 0U, + 0U, + 0U}; + } + return dispatch(request, response, workspace, access); +} + +MmsStaticDispatchResult MmsStaticApplicationDispatcher::dispatch( + const MmsConfirmedPduView& request, + const std::span response, + const std::span workspace, + const MmsStaticRequestAccessContext& access) const noexcept { + if (!objects_.valid() || !data_sets_.valid_against(objects_) || !policy_valid(policy_)) { + return make_status(MmsStaticDispatchStatus::invalid_object_table, request); + } + if (request.kind != MmsWirePduKind::confirmed_request) { + return make_status(MmsStaticDispatchStatus::malformed_request, request); + } + + switch (request.service()) { + case MmsWireConfirmedService::get_name_list: + return dispatch_get_name_list(objects_, data_sets_, policy_, request, response); + case MmsWireConfirmedService::get_variable_access_attributes: + return dispatch_attributes(objects_, request, response); + case MmsWireConfirmedService::get_named_variable_list_attributes: + return dispatch_data_set_attributes(data_sets_, request, response); + case MmsWireConfirmedService::read: + return dispatch_read(objects_, policy_, request, response, workspace); + case MmsWireConfirmedService::write: + return dispatch_write(objects_, policy_, request, response, access); + default: + return make_status(MmsStaticDispatchStatus::unsupported_service, request); + } +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_information_report.cpp b/src/mms/static_information_report.cpp new file mode 100644 index 000000000..42971f45f --- /dev/null +++ b/src/mms/static_information_report.cpp @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_information_report.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] std::span as_bytes( + const std::string_view text) noexcept { + return { + reinterpret_cast(text.data()), + text.size()}; +} + +[[nodiscard]] bool valid_mms_data( + const std::span encoded) noexcept { + asn1::BerTlvView data; + if (!asn1::BerSpanReader::try_read_exact(encoded, data) || + data.tag_class != asn1::BerClass::context_specific) { + return false; + } + if (data.tag_number == 1 || data.tag_number == 2) { + return data.constructed; + } + return data.tag_number >= 3 && data.tag_number <= 17 && !data.constructed; +} + +[[nodiscard]] MmsStaticInformationReportEncodeResult make_status( + const MmsStaticInformationReportStatus status, + const std::size_t required_bytes = 0U, + const std::size_t member_count = 0U) noexcept { + return {status, 0U, required_bytes, member_count}; +} + +} // namespace + +MmsStaticInformationReportEncodeResult +MmsStaticInformationReportEncoder::encode_data_set_snapshot_into( + const MmsStaticObjectTable& objects, + const MmsStaticDataSetTable& data_sets, + const MmsObjectNameView& data_set_name, + const MmsStaticInformationReportInput& report, + const std::span destination, + const std::span workspace) noexcept { + if (!objects.valid()) { + return make_status(MmsStaticInformationReportStatus::invalid_object_table); + } + if (!data_sets.valid()) { + return make_status(MmsStaticInformationReportStatus::invalid_data_set_table); + } + + const auto* data_set = data_sets.find(data_set_name); + if (data_set == nullptr) { + return make_status(MmsStaticInformationReportStatus::data_set_not_found); + } + if (data_set->members.empty() || + data_set->members.size() > MmsInformationReportSpanCodec::maximum_members) { + return make_status(MmsStaticInformationReportStatus::invalid_data_set_table); + } + + std::array references{}; + std::array results{}; + + std::size_t workspace_offset = 0U; + for (std::size_t index = 0U; index < data_set->members.size(); ++index) { + const auto& member = data_set->members[index]; + references[index] = MmsInformationReportReferenceInput{ + member.domain, member.item}; + + const MmsObjectNameView member_name{ + MmsObjectNameViewKind::domain_specific, + as_bytes(member.domain), + as_bytes(member.item)}; + const auto* object = objects.find(member_name); + if (object == nullptr) { + return make_status( + MmsStaticInformationReportStatus::object_not_found, + 0U, + data_set->members.size()); + } + + const auto remaining = workspace.subspan(workspace_offset); + const auto read = object->read(object->context, remaining); + if (read.status == wire::EncodeStatus::buffer_too_small) { + if (read.required_bytes > + std::numeric_limits::max() - workspace_offset) { + return make_status( + MmsStaticInformationReportStatus::backend_failure, + 0U, + data_set->members.size()); + } + return make_status( + MmsStaticInformationReportStatus::workspace_too_small, + workspace_offset + read.required_bytes, + data_set->members.size()); + } + if (!read.success() || read.bytes_written > remaining.size() || + !valid_mms_data(remaining.first(read.bytes_written))) { + return make_status( + MmsStaticInformationReportStatus::backend_failure, + 0U, + data_set->members.size()); + } + + results[index] = MmsReadAccessResultInput{ + true, + remaining.first(read.bytes_written), + 0U}; + workspace_offset += read.bytes_written; + } + + MmsInformationReportSnapshotInput wire_report; + wire_report.report_id = report.report_id; + wire_report.optional_fields = report.optional_fields; + wire_report.sequence_number = report.sequence_number; + wire_report.report_time = report.report_time; + wire_report.data_set_reference = MmsInformationReportReferenceInput{ + data_set->domain, data_set->item}; + wire_report.buffered = report.buffered; + wire_report.buffer_overflow = report.buffer_overflow; + wire_report.entry_id = report.entry_id; + wire_report.conf_revision = report.conf_revision; + wire_report.member_references = + std::span{references}.first( + data_set->members.size()); + wire_report.member_results = + std::span{results}.first( + data_set->members.size()); + wire_report.reason_for_inclusion = report.reason_for_inclusion; + + const auto encoded = MmsInformationReportSpanCodec::encode_snapshot_into( + wire_report, destination); + if (encoded.success()) { + return { + MmsStaticInformationReportStatus::response_ready, + encoded.bytes_written, + encoded.required_bytes, + data_set->members.size()}; + } + if (encoded.status == wire::EncodeStatus::buffer_too_small) { + return make_status( + MmsStaticInformationReportStatus::response_buffer_too_small, + encoded.required_bytes, + data_set->members.size()); + } + return make_status( + MmsStaticInformationReportStatus::invalid_report, + encoded.required_bytes, + data_set->members.size()); +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_object_table.cpp b/src/mms/static_object_table.cpp new file mode 100644 index 000000000..8bff516b0 --- /dev/null +++ b/src/mms/static_object_table.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_object_table.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool visible_ascii(const std::string_view value) noexcept { + if (value.empty() || value.size() > MmsServiceSpanCodec::maximum_identifier_bytes) { + return false; + } + for (const auto character : value) { + const auto byte = static_cast(character); + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] bool span_equals( + const std::span bytes, + const std::string_view text) noexcept { + if (bytes.size() != text.size()) { + return false; + } + for (std::size_t index = 0U; index < bytes.size(); ++index) { + if (bytes[index] != static_cast( + static_cast(text[index]))) { + return false; + } + } + return true; +} + +[[nodiscard]] bool valid_type_specification( + const std::span encoded) noexcept { + asn1::BerTlvView type; + return !encoded.empty() && + asn1::BerSpanReader::try_read_exact(encoded, type) && + type.tag_class == asn1::BerClass::context_specific; +} + +} // namespace + +bool MmsStaticObjectTable::valid() const noexcept { + if (objects_.empty() || objects_.size() > maximum_objects) { + return false; + } + for (std::size_t index = 0U; index < objects_.size(); ++index) { + const auto& object = objects_[index]; + if (!visible_ascii(object.domain) || !visible_ascii(object.item) || + !valid_type_specification(object.type_specification) || + object.read == nullptr) { + return false; + } + for (std::size_t other = index + 1U; other < objects_.size(); ++other) { + if (objects_[other].domain == object.domain && + objects_[other].item == object.item) { + return false; + } + } + } + return true; +} + +const MmsStaticObjectEntry* MmsStaticObjectTable::find( + const MmsObjectNameView& name) const noexcept { + if (name.kind != MmsObjectNameViewKind::domain_specific || + name.domain.empty() || name.item.empty()) { + return nullptr; + } + for (const auto& object : objects_) { + if (span_equals(name.domain, object.domain) && + span_equals(name.item, object.item)) { + return &object; + } + } + return nullptr; +} + +bool MmsStaticObjectTable::try_resolve_read_request( + const MmsReadRequestView& request, + const std::span resolved, + std::size_t& resolved_count) const noexcept { + resolved_count = 0U; + if (request.variable_count == 0U || + request.variable_count > resolved.size() || + request.variable_count > MmsServiceSpanCodec::maximum_variables) { + return false; + } + for (std::size_t index = 0U; index < request.variable_count; ++index) { + MmsObjectNameView name; + if (!request.try_variable(index, name)) { + resolved_count = 0U; + return false; + } + resolved[index] = find(name); + ++resolved_count; + } + return true; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_report_connection.cpp b/src/mms/static_report_connection.cpp new file mode 100644 index 000000000..b65961490 --- /dev/null +++ b/src/mms/static_report_connection.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_report_connection.hpp" + +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool add_size( + const std::size_t base, + const std::size_t extra, + std::size_t& total) noexcept { + if (extra > std::numeric_limits::max() - base) { + total = 0U; + return false; + } + total = base + extra; + return true; +} + +[[nodiscard]] MmsStaticReportConnectionResult make_result( + const MmsStaticReportConnectionStatus status, + const MmsStaticUrcbStatus urcb_status, + const MmsStaticUrcbEmissionPlan& plan = {}) noexcept { + MmsStaticReportConnectionResult result; + result.status = status; + result.urcb_status = urcb_status; + result.control_block_index = plan.index; + result.sequence_number = plan.sequence_number; + result.reason = plan.reason; + return result; +} + +[[nodiscard]] bool final_frame_size( + const std::uint32_t presentation_context_id, + const std::size_t mms_bytes, + std::size_t& required) noexcept { + const auto fully_encoded = osi::PresentationSpanCodec::fully_encoded_data_size( + presentation_context_id, mms_bytes); + return fully_encoded && add_size(*fully_encoded, 11U, required); +} + +[[nodiscard]] MmsStaticReportConnectionResult response_capacity( + const MmsStaticUrcbEmissionPlan& plan, + const MmsStaticUrcbStatus urcb_status, + const std::size_t required) noexcept { + auto result = make_result( + MmsStaticReportConnectionStatus::response_buffer_too_small, + urcb_status, + plan); + result.required_response_bytes = required; + return result; +} + +[[nodiscard]] MmsStaticReportConnectionResult workspace_capacity( + const MmsStaticUrcbEmissionPlan& plan, + const MmsStaticUrcbStatus urcb_status, + const std::size_t required) noexcept { + auto result = make_result( + MmsStaticReportConnectionStatus::workspace_too_small, + urcb_status, + plan); + result.required_workspace_bytes = required; + return result; +} + +} // namespace + +MmsStaticReportConnectionResult MmsStaticReportConnection::poll( + const MmsStaticConnectionRuntime& connection, + MmsStaticUrcbRuntime& reports, + const std::uint64_t now_ms, + const std::span report_time, + const std::span response, + const std::span workspace) noexcept { + if (connection.state() != MmsStaticConnectionState::established || + connection.mms_presentation_context_id() == 0U) { + return make_result( + MmsStaticReportConnectionStatus::not_established, + MmsStaticUrcbStatus::temporarily_unavailable); + } + + MmsStaticUrcbEmissionPlan plan; + if (!reports.next_due(now_ms, plan)) { + return make_result( + MmsStaticReportConnectionStatus::no_report_due, + reports.valid() + ? MmsStaticUrcbStatus::no_report_due + : MmsStaticUrcbStatus::invalid_runtime); + } + + // Raw MMS report is written into response. The report encoder may use the + // caller workspace transiently for DataSet member values. Only after the + // raw PDU succeeds is workspace reused for P-DATA and final TPKT staging. + const auto encoded = reports.encode(plan, report_time, response, workspace); + if (!encoded.success()) { + if (encoded.status == MmsStaticUrcbStatus::response_buffer_too_small) { + std::size_t final_required{}; + if (!final_frame_size( + connection.mms_presentation_context_id(), + encoded.required_bytes, + final_required)) { + return make_result( + MmsStaticReportConnectionStatus::report_encode_failed, + MmsStaticUrcbStatus::report_encode_failed, + plan); + } + return response_capacity(plan, encoded.status, final_required); + } + if (encoded.status == MmsStaticUrcbStatus::workspace_too_small) { + return workspace_capacity(plan, encoded.status, encoded.required_bytes); + } + return make_result( + MmsStaticReportConnectionStatus::report_encode_failed, + encoded.status, + plan); + } + + const auto p_data = osi::PresentationSpanCodec::encode_p_data_into( + response.first(encoded.bytes_written), + workspace, + connection.mms_presentation_context_id(), + true); + if (!p_data.success()) { + if (p_data.status == wire::EncodeStatus::buffer_too_small) { + std::size_t final_required{}; + if (!add_size(p_data.required_bytes, 7U, final_required)) { + return make_result( + MmsStaticReportConnectionStatus::report_encode_failed, + MmsStaticUrcbStatus::report_encode_failed, + plan); + } + auto result = workspace_capacity( + plan, MmsStaticUrcbStatus::workspace_too_small, final_required); + result.required_response_bytes = final_required; + return result; + } + return make_result( + MmsStaticReportConnectionStatus::report_encode_failed, + MmsStaticUrcbStatus::report_encode_failed, + plan); + } + + std::size_t final_required{}; + if (!add_size(p_data.bytes_written, 7U, final_required)) { + return make_result( + MmsStaticReportConnectionStatus::report_encode_failed, + MmsStaticUrcbStatus::report_encode_failed, + plan); + } + if (response.size() < final_required) { + return response_capacity( + plan, MmsStaticUrcbStatus::response_buffer_too_small, final_required); + } + if (workspace.size() < final_required) { + auto result = workspace_capacity( + plan, MmsStaticUrcbStatus::workspace_too_small, final_required); + result.required_response_bytes = final_required; + return result; + } + + // P-DATA currently resides in workspace. COTP consumes it into response; + // after that source is no longer needed, so workspace can safely become + // the TPKT staging destination. + const auto cotp = osi::CotpSpanCodec::encode_data_into( + workspace.first(p_data.bytes_written), + response.first(final_required - osi::TpktSpanCodec::header_length)); + if (!cotp.success()) { + return make_result( + MmsStaticReportConnectionStatus::report_encode_failed, + MmsStaticUrcbStatus::report_encode_failed, + plan); + } + + const auto tpkt = osi::TpktSpanCodec::encode_into( + response.first(cotp.bytes_written), + workspace.first(final_required)); + if (!tpkt.success() || tpkt.bytes_written != final_required) { + return make_result( + MmsStaticReportConnectionStatus::report_encode_failed, + MmsStaticUrcbStatus::report_encode_failed, + plan); + } + + // Commit only after the complete TPKT image exists. If capacity failed at + // any earlier stage, the same plan/SqNum can be retried without a gap. + const auto committed = reports.commit(plan, now_ms); + if (committed != MmsStaticUrcbStatus::ok) { + return make_result( + MmsStaticReportConnectionStatus::stale_plan, + committed, + plan); + } + + std::copy_n(workspace.begin(), tpkt.bytes_written, response.begin()); + auto result = make_result( + MmsStaticReportConnectionStatus::response_ready, + MmsStaticUrcbStatus::ok, + plan); + result.bytes_written = tpkt.bytes_written; + result.required_response_bytes = final_required; + result.required_workspace_bytes = final_required; + return result; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_server_session.cpp b/src/mms/static_server_session.cpp new file mode 100644 index 000000000..76dee6357 --- /dev/null +++ b/src/mms/static_server_session.cpp @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_server_session.hpp" + +#include + +namespace ar::iec61850::mms { + +MmsStaticServerSessionResult MmsStaticServerSession::make_result( + const MmsStaticServerSessionStatus status, + const MmsStaticConnectionStatus connection_status, + const std::size_t bytes_received, + const std::size_t bytes_sent) const noexcept { + return { + status, + connection_status, + bytes_received, + bytes_sent, + receive_size_, + response_size_ - response_offset_}; +} + +void MmsStaticServerSession::consume_input(const std::size_t consumed) noexcept { + if (consumed == 0U || consumed > receive_size_) { + return; + } + const auto remaining = receive_size_ - consumed; + if (remaining != 0U) { + std::memmove( + buffers_.receive.data(), + buffers_.receive.data() + consumed, + remaining); + } + receive_size_ = remaining; +} + +void MmsStaticServerSession::reset() noexcept { + receive_size_ = 0U; + response_offset_ = 0U; + response_size_ = 0U; + terminal_ = false; + runtime_.reset(); +} + +MmsStaticServerSessionResult MmsStaticServerSession::poll_once() noexcept { + const auto terminate = [this]( + const MmsStaticServerSessionStatus status, + const MmsStaticConnectionStatus connection_status, + const std::size_t bytes_received = 0U, + const std::size_t bytes_sent = 0U) noexcept { + // All terminal transport/session outcomes are association teardown + // boundaries. MmsStaticConnectionRuntime guards its lifecycle callback, + // so this remains exactly-once even when process_tcp_window() already + // observed a COTP disconnect before the session reaches this helper. + runtime_.close_transport(); + terminal_ = true; + return make_result(status, connection_status, bytes_received, bytes_sent); + }; + + if (!buffers_.valid() || stream_.send_callback == nullptr || + stream_.receive_callback == nullptr) { + return terminate( + MmsStaticServerSessionStatus::invalid_configuration, + MmsStaticConnectionStatus::backend_failure); + } + if (terminal_) { + return make_result( + MmsStaticServerSessionStatus::protocol_error, + MmsStaticConnectionStatus::closed); + } + + if (response_offset_ < response_size_) { + const auto remaining = std::span{buffers_.response} + .subspan(response_offset_, response_size_ - response_offset_); + const auto sent = stream_.send(remaining); + if (sent.status == embedded::IoStatus::would_block) { + return make_result( + MmsStaticServerSessionStatus::would_block, + MmsStaticConnectionStatus::response_ready); + } + if (sent.status == embedded::IoStatus::timeout) { + return make_result( + MmsStaticServerSessionStatus::timed_out, + MmsStaticConnectionStatus::response_ready); + } + if (sent.status == embedded::IoStatus::closed) { + return terminate( + MmsStaticServerSessionStatus::peer_closed, + MmsStaticConnectionStatus::closed); + } + if (!sent.success() || sent.transferred == 0U || + sent.transferred > remaining.size()) { + return terminate( + MmsStaticServerSessionStatus::transport_error, + MmsStaticConnectionStatus::backend_failure); + } + + response_offset_ += sent.transferred; + if (response_offset_ == response_size_) { + response_offset_ = 0U; + response_size_ = 0U; + } + return make_result( + MmsStaticServerSessionStatus::progressed, + MmsStaticConnectionStatus::response_ready, + 0U, + sent.transferred); + } + + if (receive_size_ != 0U) { + const auto connection = runtime_.process_tcp_window( + std::span{buffers_.receive}.first(receive_size_), + buffers_.response, + buffers_.workspace); + + switch (connection.status) { + case MmsStaticConnectionStatus::response_ready: + consume_input(connection.consumed_bytes); + response_offset_ = 0U; + response_size_ = connection.bytes_written; + { + auto result = make_result( + MmsStaticServerSessionStatus::response_pending, + connection.status); + result.application_status = connection.application_status; + result.application_service = connection.application_service; + result.invoke_id = connection.invoke_id; + return result; + } + case MmsStaticConnectionStatus::consumed_no_response: + consume_input(connection.consumed_bytes); + return make_result( + MmsStaticServerSessionStatus::progressed, + connection.status); + case MmsStaticConnectionStatus::application_rejected: + consume_input(connection.consumed_bytes); + { + auto result = make_result( + MmsStaticServerSessionStatus::application_rejected, + connection.status); + result.application_status = connection.application_status; + result.application_service = connection.application_service; + result.invoke_id = connection.invoke_id; + return result; + } + case MmsStaticConnectionStatus::closed: + consume_input(connection.consumed_bytes); + return terminate( + MmsStaticServerSessionStatus::peer_closed, + connection.status); + case MmsStaticConnectionStatus::need_more: + break; + case MmsStaticConnectionStatus::malformed_transport: + case MmsStaticConnectionStatus::protocol_violation: + case MmsStaticConnectionStatus::response_buffer_too_small: + case MmsStaticConnectionStatus::workspace_too_small: + case MmsStaticConnectionStatus::peer_limit_exceeded: + case MmsStaticConnectionStatus::backend_failure: + return terminate( + MmsStaticServerSessionStatus::protocol_error, + connection.status); + } + } + + if (receive_size_ == buffers_.receive.size()) { + return terminate( + MmsStaticServerSessionStatus::receive_buffer_full, + MmsStaticConnectionStatus::need_more); + } + + const auto destination = buffers_.receive.subspan(receive_size_); + const auto received = stream_.receive(destination); + if (received.status == embedded::IoStatus::would_block) { + return make_result( + MmsStaticServerSessionStatus::would_block, + MmsStaticConnectionStatus::need_more); + } + if (received.status == embedded::IoStatus::timeout) { + return make_result( + MmsStaticServerSessionStatus::timed_out, + MmsStaticConnectionStatus::need_more); + } + if (received.status == embedded::IoStatus::closed) { + return terminate( + MmsStaticServerSessionStatus::peer_closed, + MmsStaticConnectionStatus::closed); + } + if (!received.success() || received.transferred == 0U || + received.transferred > destination.size()) { + return terminate( + MmsStaticServerSessionStatus::transport_error, + MmsStaticConnectionStatus::backend_failure); + } + + receive_size_ += received.transferred; + return make_result( + MmsStaticServerSessionStatus::progressed, + MmsStaticConnectionStatus::need_more, + received.transferred, + 0U); +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_urcb_objects.cpp b/src/mms/static_urcb_objects.cpp new file mode 100644 index 000000000..0f5d4f733 --- /dev/null +++ b/src/mms/static_urcb_objects.cpp @@ -0,0 +1,630 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_urcb_objects.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kUnsigned32Type{0x86U, 0x01U, 0x20U}; +constexpr std::array kVisible255Type{0x8AU, 0x02U, 0x00U, 0xFFU}; +constexpr std::array kBitString10Type{0x84U, 0x01U, 0x0AU}; +constexpr std::array kBitString6Type{0x84U, 0x01U, 0x06U}; + +constexpr std::array + kAttributes{ + MmsStaticUrcbAttribute::report_id, + MmsStaticUrcbAttribute::report_enabled, + MmsStaticUrcbAttribute::reserved, + MmsStaticUrcbAttribute::data_set, + MmsStaticUrcbAttribute::conf_revision, + MmsStaticUrcbAttribute::optional_fields, + MmsStaticUrcbAttribute::buffer_time, + MmsStaticUrcbAttribute::trigger_options, + MmsStaticUrcbAttribute::integrity_period, + MmsStaticUrcbAttribute::general_interrogation, + MmsStaticUrcbAttribute::sequence_number}; + +constexpr std::array + kSuffixes{ + "RptID", + "RptEna", + "Resv", + "DatSet", + "ConfRev", + "OptFlds", + "BufTm", + "TrgOps", + "IntgPd", + "GI", + "SqNum"}; + +constexpr std::uint32_t kHardwareFault = 1U; +constexpr std::uint32_t kTemporarilyUnavailable = 2U; +constexpr std::uint32_t kObjectAccessDenied = 3U; +constexpr std::uint32_t kTypeInconsistent = 7U; +constexpr std::uint32_t kObjectNonExistent = 10U; +constexpr std::uint32_t kObjectValueInvalid = 11U; + +[[nodiscard]] std::span as_bytes( + const std::string_view text) noexcept { + return { + reinterpret_cast(text.data()), + text.size()}; +} + +[[nodiscard]] bool visible_ascii(const std::span bytes) noexcept { + if (bytes.empty() || bytes.size() > MmsServiceSpanCodec::maximum_identifier_bytes) { + return false; + } + for (const auto byte : bytes) { + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] std::span type_for( + const MmsStaticUrcbAttribute attribute) noexcept { + switch (attribute) { + case MmsStaticUrcbAttribute::report_id: + case MmsStaticUrcbAttribute::data_set: + return kVisible255Type; + case MmsStaticUrcbAttribute::report_enabled: + case MmsStaticUrcbAttribute::reserved: + case MmsStaticUrcbAttribute::general_interrogation: + return kBooleanType; + case MmsStaticUrcbAttribute::conf_revision: + case MmsStaticUrcbAttribute::buffer_time: + case MmsStaticUrcbAttribute::integrity_period: + case MmsStaticUrcbAttribute::sequence_number: + return kUnsigned32Type; + case MmsStaticUrcbAttribute::optional_fields: + return kBitString10Type; + case MmsStaticUrcbAttribute::trigger_options: + return kBitString6Type; + } + return {}; +} + +[[nodiscard]] bool writable_attribute( + const MmsStaticUrcbAttribute attribute) noexcept { + return attribute != MmsStaticUrcbAttribute::conf_revision && + attribute != MmsStaticUrcbAttribute::sequence_number; +} + +[[nodiscard]] wire::EncodeResult capacity_result( + const std::size_t required, + const std::span destination) noexcept { + return destination.size() < required + ? wire::EncodeResult{wire::EncodeStatus::buffer_too_small, 0U, required} + : wire::EncodeResult{wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] wire::EncodeResult encode_boolean( + const bool value, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + const auto capacity = capacity_result(required, destination); + if (!capacity.success()) { + return capacity; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return capacity; +} + +[[nodiscard]] std::size_t unsigned_content_size(std::uint32_t value) noexcept { + std::size_t size = 1U; + while (value > 0xFFU) { + ++size; + value >>= 8U; + } + return size; +} + +[[nodiscard]] wire::EncodeResult encode_unsigned( + const std::uint32_t value, + const std::span destination) noexcept { + const auto content_size = unsigned_content_size(value); + const auto total = asn1::BerSpanWriter::tlv_size(6, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 6, content_size)) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + for (std::size_t index = content_size; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast((value >> shift) & 0xFFU))) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + } + return writer.size() == *total + ? capacity + : wire::EncodeResult{wire::EncodeStatus::value_out_of_range, 0U, *total}; +} + +[[nodiscard]] wire::EncodeResult encode_visible( + const std::string_view text, + const std::span destination) noexcept { + const auto total = asn1::BerSpanWriter::tlv_size(10, text.size()); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, text.size()) || + !writer.write_bytes(as_bytes(text)) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_data_set_reference( + const std::string_view domain, + const std::string_view item, + const std::span destination) noexcept { + if (domain.size() > std::numeric_limits::max() - 1U - item.size()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content_size = domain.size() + 1U + item.size(); + const auto total = asn1::BerSpanWriter::tlv_size(10, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 10, content_size) || + !writer.write_bytes(as_bytes(domain)) || + !writer.write_byte(static_cast('/')) || + !writer.write_bytes(as_bytes(item)) || writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult encode_bit_string( + const std::uint8_t unused_bits, + const std::span bytes, + const std::span destination) noexcept { + if (unused_bits > 7U || bytes.size() == std::numeric_limits::max()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto content_size = bytes.size() + 1U; + const auto total = asn1::BerSpanWriter::tlv_size(4, content_size); + if (!total) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto capacity = capacity_result(*total, destination); + if (!capacity.success()) { + return capacity; + } + asn1::BerSpanWriter writer{destination.first(*total)}; + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 4, content_size) || + !writer.write_byte(unused_bits) || !writer.write_bytes(bytes) || + writer.size() != *total) { + return {wire::EncodeStatus::value_out_of_range, 0U, *total}; + } + return capacity; +} + +[[nodiscard]] wire::EncodeResult read_urcb_attribute( + const void* raw_context, + const std::span destination) noexcept { + const auto* context = static_cast(raw_context); + if (context == nullptr || context->runtime == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto* state = context->runtime->state(context->index); + if (state == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + switch (context->attribute) { + case MmsStaticUrcbAttribute::report_id: + return encode_visible(state->report_id(), destination); + case MmsStaticUrcbAttribute::report_enabled: + return encode_boolean(state->enabled, destination); + case MmsStaticUrcbAttribute::reserved: + return encode_boolean(state->reserved, destination); + case MmsStaticUrcbAttribute::data_set: + return encode_data_set_reference( + state->data_set_domain(), state->data_set_item(), destination); + case MmsStaticUrcbAttribute::conf_revision: + return encode_unsigned(state->conf_revision, destination); + case MmsStaticUrcbAttribute::optional_fields: + return encode_bit_string(6U, state->optional_fields, destination); + case MmsStaticUrcbAttribute::buffer_time: + return encode_unsigned(state->buffer_time_ms, destination); + case MmsStaticUrcbAttribute::trigger_options: { + const std::array trigger{state->trigger_options}; + return encode_bit_string(2U, trigger, destination); + } + case MmsStaticUrcbAttribute::integrity_period: + return encode_unsigned(state->integrity_period_ms, destination); + case MmsStaticUrcbAttribute::general_interrogation: + return encode_boolean(state->general_interrogation_pending, destination); + case MmsStaticUrcbAttribute::sequence_number: + return encode_unsigned(state->sequence_number, destination); + } + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; +} + +[[nodiscard]] bool decode_boolean( + const std::span encoded, + bool& value) noexcept { + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 3 || tlv.constructed || tlv.value.size() != 1U) { + return false; + } + value = tlv.value[0] != 0U; + return true; +} + +[[nodiscard]] bool decode_unsigned( + const std::span encoded, + std::uint32_t& value) noexcept { + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 6 || tlv.constructed) { + return false; + } + const auto decoded = asn1::BerSpanReader::read_uint32(tlv); + if (!decoded) { + return false; + } + value = *decoded; + return true; +} + +[[nodiscard]] bool decode_visible( + const std::span encoded, + std::string_view& value) noexcept { + value = {}; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 10 || tlv.constructed || !visible_ascii(tlv.value)) { + return false; + } + value = { + reinterpret_cast(tlv.value.data()), + tlv.value.size()}; + return true; +} + +[[nodiscard]] bool decode_bit_string( + const std::span encoded, + const std::uint8_t expected_unused_bits, + const std::size_t expected_bytes, + std::span& bytes) noexcept { + bytes = {}; + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(encoded, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 4 || tlv.constructed || + tlv.value.size() != expected_bytes + 1U || + tlv.value[0] != expected_unused_bits) { + return false; + } + bytes = tlv.value.subspan(1U); + if (!bytes.empty() && expected_unused_bits != 0U) { + const auto mask = static_cast((1U << expected_unused_bits) - 1U); + if ((bytes.back() & mask) != 0U) { + bytes = {}; + return false; + } + } + return true; +} + +[[nodiscard]] bool split_reference( + const std::string_view reference, + std::string_view& domain, + std::string_view& item) noexcept { + domain = {}; + item = {}; + std::size_t slash = reference.size(); + for (std::size_t index = 0U; index < reference.size(); ++index) { + if (reference[index] != '/') { + continue; + } + if (slash != reference.size()) { + return false; + } + slash = index; + } + if (slash == 0U || slash >= reference.size() - 1U) { + return false; + } + domain = reference.substr(0U, slash); + item = reference.substr(slash + 1U); + return true; +} + +[[nodiscard]] MmsStaticWriteResult map_write_status( + const MmsStaticUrcbStatus status) noexcept { + switch (status) { + case MmsStaticUrcbStatus::ok: + return {true, 0U}; + case MmsStaticUrcbStatus::object_access_denied: + return {false, kObjectAccessDenied}; + case MmsStaticUrcbStatus::temporarily_unavailable: + return {false, kTemporarilyUnavailable}; + case MmsStaticUrcbStatus::data_set_not_found: + return {false, kObjectNonExistent}; + case MmsStaticUrcbStatus::invalid_value: + return {false, kObjectValueInvalid}; + case MmsStaticUrcbStatus::invalid_runtime: + case MmsStaticUrcbStatus::index_out_of_range: + case MmsStaticUrcbStatus::no_report_due: + case MmsStaticUrcbStatus::stale_plan: + case MmsStaticUrcbStatus::workspace_too_small: + case MmsStaticUrcbStatus::backend_failure: + case MmsStaticUrcbStatus::response_buffer_too_small: + case MmsStaticUrcbStatus::report_encode_failed: + return {false, kHardwareFault}; + } + return {false, kHardwareFault}; +} + +[[nodiscard]] MmsStaticWriteResult write_urcb_attribute( + void* raw_context, + const std::span encoded_data) noexcept { + auto* context = static_cast(raw_context); + if (context == nullptr || context->runtime == nullptr) { + return {false, kHardwareFault}; + } + + switch (context->attribute) { + case MmsStaticUrcbAttribute::report_id: { + std::string_view value; + if (!decode_visible(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + return map_write_status(context->runtime->set_report_id(context->index, value)); + } + case MmsStaticUrcbAttribute::report_enabled: { + bool value = false; + if (!decode_boolean(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + const auto now = context->now_ms == nullptr + ? std::uint64_t{0U} + : context->now_ms(context->now_context); + return map_write_status( + context->runtime->set_enabled(context->index, value, now)); + } + case MmsStaticUrcbAttribute::reserved: { + bool value = false; + if (!decode_boolean(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + return map_write_status( + context->runtime->set_reserved(context->index, value)); + } + case MmsStaticUrcbAttribute::data_set: { + std::string_view reference; + std::string_view domain; + std::string_view item; + if (!decode_visible(encoded_data, reference) || + !split_reference(reference, domain, item)) { + return {false, kTypeInconsistent}; + } + return map_write_status( + context->runtime->set_data_set(context->index, domain, item)); + } + case MmsStaticUrcbAttribute::optional_fields: { + std::span bytes; + if (!decode_bit_string(encoded_data, 6U, 2U, bytes)) { + return {false, kTypeInconsistent}; + } + return map_write_status( + context->runtime->set_optional_fields(context->index, bytes)); + } + case MmsStaticUrcbAttribute::buffer_time: { + std::uint32_t value = 0U; + if (!decode_unsigned(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + return map_write_status( + context->runtime->set_buffer_time_ms(context->index, value)); + } + case MmsStaticUrcbAttribute::trigger_options: { + std::span bytes; + if (!decode_bit_string(encoded_data, 2U, 1U, bytes)) { + return {false, kTypeInconsistent}; + } + return map_write_status( + context->runtime->set_trigger_options(context->index, bytes[0])); + } + case MmsStaticUrcbAttribute::integrity_period: { + std::uint32_t value = 0U; + if (!decode_unsigned(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + return map_write_status( + context->runtime->set_integrity_period_ms(context->index, value)); + } + case MmsStaticUrcbAttribute::general_interrogation: { + bool value = false; + if (!decode_boolean(encoded_data, value)) { + return {false, kTypeInconsistent}; + } + return value + ? map_write_status( + context->runtime->request_general_interrogation(context->index)) + : MmsStaticWriteResult{true, 0U}; + } + case MmsStaticUrcbAttribute::conf_revision: + case MmsStaticUrcbAttribute::sequence_number: + return {false, kObjectAccessDenied}; + } + return {false, kHardwareFault}; +} + +} // namespace + +std::size_t MmsStaticUrcbObjectBank::required_context_capacity() const noexcept { + if (runtime_ == nullptr || !runtime_->valid() || + runtime_->size() > std::numeric_limits::max() / + attributes_per_control_block) { + return std::numeric_limits::max(); + } + return runtime_->size() * attributes_per_control_block; +} + +std::size_t MmsStaticUrcbObjectBank::required_object_capacity() const noexcept { + const auto contexts = required_context_capacity(); + if (contexts == std::numeric_limits::max() || + contexts > std::numeric_limits::max() - base_objects_.size()) { + return std::numeric_limits::max(); + } + return base_objects_.size() + contexts; +} + +std::size_t MmsStaticUrcbObjectBank::required_name_bytes() const noexcept { + if (runtime_ == nullptr || !runtime_->valid()) { + return std::numeric_limits::max(); + } + std::size_t total = 0U; + for (std::size_t index = 0U; index < runtime_->size(); ++index) { + const auto* definition = runtime_->definition(index); + if (definition == nullptr) { + return std::numeric_limits::max(); + } + for (const auto suffix : kSuffixes) { + if (definition->item.size() > + MmsServiceSpanCodec::maximum_identifier_bytes - 1U - suffix.size()) { + return std::numeric_limits::max(); + } + const auto name_size = definition->item.size() + 1U + suffix.size(); + if (name_size > std::numeric_limits::max() - total) { + return std::numeric_limits::max(); + } + total += name_size; + } + } + return total; +} + +bool MmsStaticUrcbObjectBank::initialize() noexcept { + initialized_ = false; + object_count_ = 0U; + name_bytes_used_ = 0U; + table_ = MmsStaticObjectTable{std::span{}}; + + if (runtime_ == nullptr || !runtime_->valid()) { + return false; + } + const auto required_objects = required_object_capacity(); + const auto required_contexts = required_context_capacity(); + const auto required_names = required_name_bytes(); + if (required_objects == std::numeric_limits::max() || + required_contexts == std::numeric_limits::max() || + required_names == std::numeric_limits::max() || + required_objects > object_storage_.size() || + required_objects > MmsStaticObjectTable::maximum_objects || + required_contexts > context_storage_.size() || + required_names > name_storage_.size()) { + return false; + } + + std::copy(base_objects_.begin(), base_objects_.end(), object_storage_.begin()); + std::size_t object_offset = base_objects_.size(); + std::size_t context_offset = 0U; + std::size_t name_offset = 0U; + + for (std::size_t urcb_index = 0U; urcb_index < runtime_->size(); ++urcb_index) { + const auto* definition = runtime_->definition(urcb_index); + if (definition == nullptr) { + return false; + } + for (std::size_t attribute_index = 0U; + attribute_index < attributes_per_control_block; + ++attribute_index) { + const auto suffix = kSuffixes[attribute_index]; + const auto name_size = definition->item.size() + 1U + suffix.size(); + auto* name = name_storage_.data() + name_offset; + std::copy(definition->item.begin(), definition->item.end(), name); + name[definition->item.size()] = '$'; + std::copy( + suffix.begin(), + suffix.end(), + name + definition->item.size() + 1U); + + auto& context = context_storage_[context_offset]; + context = MmsStaticUrcbObjectContext{ + runtime_, + urcb_index, + kAttributes[attribute_index], + now_ms_, + now_context_}; + + const auto attribute = kAttributes[attribute_index]; + object_storage_[object_offset] = MmsStaticObjectEntry{ + definition->domain, + std::string_view{name, name_size}, + type_for(attribute), + read_urcb_attribute, + &context, + false, + writable_attribute(attribute) ? write_urcb_attribute : nullptr, + writable_attribute(attribute) ? &context : nullptr}; + + name_offset += name_size; + ++context_offset; + ++object_offset; + } + } + + table_ = MmsStaticObjectTable{ + std::span{object_storage_.data(), object_offset}}; + if (!table_.valid()) { + table_ = MmsStaticObjectTable{std::span{}}; + return false; + } + + object_count_ = object_offset; + name_bytes_used_ = name_offset; + initialized_ = true; + return true; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_urcb_runtime.cpp b/src/mms/static_urcb_runtime.cpp new file mode 100644 index 000000000..968cd8a2c --- /dev/null +++ b/src/mms/static_urcb_runtime.cpp @@ -0,0 +1,612 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_urcb_runtime.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +constexpr std::uint8_t kAllowedOptionalFirst = 0x7CU; +constexpr std::uint8_t kAllowedOptionalSecond = 0x80U; +constexpr std::uint8_t kAllowedTriggerOptions = 0x7CU; +constexpr std::uint8_t kTriggerIntegrity = 0x08U; + +[[nodiscard]] bool visible_ascii( + const std::string_view text, + const std::size_t maximum_bytes) noexcept { + if (text.empty() || text.size() > maximum_bytes) { + return false; + } + for (const auto character : text) { + const auto byte = static_cast(character); + if (byte == 0U || byte > 0x7FU) { + return false; + } + } + return true; +} + +[[nodiscard]] bool valid_report_id(const std::string_view report_id) noexcept { + return visible_ascii( + report_id, + MmsStaticUrcbState::maximum_report_id_bytes); +} + +[[nodiscard]] bool valid_reference( + const std::string_view domain, + const std::string_view item) noexcept { + return visible_ascii( + domain, + MmsStaticUrcbState::maximum_reference_component_bytes) && + visible_ascii( + item, + MmsStaticUrcbState::maximum_reference_component_bytes) && + domain.size() + 1U + item.size() <= + MmsInformationReportSpanCodec::maximum_reference_bytes; +} + +[[nodiscard]] bool valid_optional_fields( + const std::span optional_fields) noexcept { + return optional_fields.size() == + MmsInformationReportSpanCodec::optional_field_bytes && + (optional_fields[0] & static_cast(~kAllowedOptionalFirst)) == 0U && + (optional_fields[1] & static_cast(~kAllowedOptionalSecond)) == 0U; +} + +[[nodiscard]] bool valid_trigger_options(const std::uint8_t options) noexcept { + return (options & static_cast(~kAllowedTriggerOptions)) == 0U; +} + +template +[[nodiscard]] bool copy_text( + const std::string_view source, + std::array& destination, + std::size_t& length) noexcept { + if (source.empty() || source.size() > destination.size()) { + length = 0U; + return false; + } + std::fill(destination.begin(), destination.end(), '\0'); + std::copy(source.begin(), source.end(), destination.begin()); + length = source.size(); + return true; +} + +[[nodiscard]] std::span as_bytes( + const std::string_view text) noexcept { + return { + reinterpret_cast(text.data()), + text.size()}; +} + +[[nodiscard]] MmsObjectNameView object_name( + const std::string_view domain, + const std::string_view item) noexcept { + return { + MmsObjectNameViewKind::domain_specific, + as_bytes(domain), + as_bytes(item)}; +} + +[[nodiscard]] bool same_name( + const MmsStaticUrcbDefinition& left, + const MmsStaticUrcbDefinition& right) noexcept { + return left.domain == right.domain && left.item == right.item; +} + +void bump_revision(MmsStaticUrcbState& state) noexcept { + state.revision = state.revision == std::numeric_limits::max() + ? 1U + : state.revision + 1U; +} + +[[nodiscard]] std::uint64_t saturating_add( + const std::uint64_t base, + const std::uint64_t delta) noexcept { + return delta > std::numeric_limits::max() - base + ? std::numeric_limits::max() + : base + delta; +} + +[[nodiscard]] std::uint32_t effective_integrity_period( + const MmsStaticUrcbState& state) noexcept { + if (state.integrity_period_ms == 0U) { + return 0U; + } + return state.integrity_period_ms < MmsStaticUrcbRuntime::minimum_integrity_period_ms + ? MmsStaticUrcbRuntime::minimum_integrity_period_ms + : state.integrity_period_ms; +} + +void refresh_integrity_arm( + MmsStaticUrcbState& state, + const std::uint64_t now_ms) noexcept { + const auto period = effective_integrity_period(state); + const bool selected = (state.trigger_options & kTriggerIntegrity) != 0U; + state.integrity_armed = state.enabled && selected && period != 0U; + state.next_integrity_due_ms = state.integrity_armed + ? saturating_add(now_ms, period) + : 0U; +} + +[[nodiscard]] std::uint8_t next_sequence_number( + const std::uint8_t current) noexcept { + return static_cast( + (static_cast(current) + 1U) & 0xFFU); +} + +[[nodiscard]] bool plan_matches_state( + const MmsStaticUrcbEmissionPlan& plan, + const MmsStaticUrcbState& state) noexcept { + if (!state.enabled || plan.revision != state.revision || + plan.reason == MmsStaticUrcbReportReason::none || + plan.sequence_number != next_sequence_number(state.sequence_number)) { + return false; + } + if (plan.reason == MmsStaticUrcbReportReason::general_interrogation) { + return state.general_interrogation_pending; + } + return plan.reason == MmsStaticUrcbReportReason::integrity && state.integrity_armed; +} + +[[nodiscard]] MmsStaticUrcbEncodeResult map_report_result( + const MmsStaticInformationReportEncodeResult& result) noexcept { + MmsStaticUrcbStatus status = MmsStaticUrcbStatus::report_encode_failed; + switch (result.status) { + case MmsStaticInformationReportStatus::response_ready: + status = MmsStaticUrcbStatus::ok; + break; + case MmsStaticInformationReportStatus::data_set_not_found: + status = MmsStaticUrcbStatus::data_set_not_found; + break; + case MmsStaticInformationReportStatus::workspace_too_small: + status = MmsStaticUrcbStatus::workspace_too_small; + break; + case MmsStaticInformationReportStatus::backend_failure: + case MmsStaticInformationReportStatus::object_not_found: + status = MmsStaticUrcbStatus::backend_failure; + break; + case MmsStaticInformationReportStatus::response_buffer_too_small: + status = MmsStaticUrcbStatus::response_buffer_too_small; + break; + case MmsStaticInformationReportStatus::invalid_object_table: + case MmsStaticInformationReportStatus::invalid_data_set_table: + case MmsStaticInformationReportStatus::invalid_report: + status = MmsStaticUrcbStatus::report_encode_failed; + break; + } + return {status, result.bytes_written, result.required_bytes, result.member_count}; +} + +} // namespace + +bool MmsStaticUrcbRuntime::initialize() noexcept { + initialized_ = false; + if (objects_ == nullptr || data_sets_ == nullptr || + !objects_->valid() || !data_sets_->valid() || + !data_sets_->valid_against(*objects_) || + definitions_.size() > maximum_control_blocks || + states_.size() < definitions_.size()) { + return false; + } + + for (std::size_t index = 0U; index < definitions_.size(); ++index) { + const auto& definition = definitions_[index]; + if (!valid_reference(definition.domain, definition.item) || + !valid_report_id(definition.report_id) || + !valid_reference(definition.data_set_domain, definition.data_set_item) || + !valid_optional_fields(definition.optional_fields) || + !valid_trigger_options(definition.trigger_options) || + data_sets_->find(object_name( + definition.data_set_domain, + definition.data_set_item)) == nullptr) { + return false; + } + for (std::size_t earlier = 0U; earlier < index; ++earlier) { + if (same_name(definitions_[earlier], definition)) { + return false; + } + } + } + + for (std::size_t index = 0U; index < definitions_.size(); ++index) { + const auto& definition = definitions_[index]; + auto& state_ref = states_[index]; + state_ref = {}; + if (!copy_text( + definition.report_id, + state_ref.report_id_storage, + state_ref.report_id_size) || + !copy_text( + definition.data_set_domain, + state_ref.data_set_domain_storage, + state_ref.data_set_domain_size) || + !copy_text( + definition.data_set_item, + state_ref.data_set_item_storage, + state_ref.data_set_item_size)) { + return false; + } + state_ref.optional_fields = definition.optional_fields; + state_ref.conf_revision = definition.conf_revision; + state_ref.buffer_time_ms = definition.buffer_time_ms; + state_ref.trigger_options = definition.trigger_options; + state_ref.integrity_period_ms = definition.integrity_period_ms; + state_ref.revision = 1U; + } + + initialized_ = true; + return true; +} + +const MmsStaticUrcbDefinition* MmsStaticUrcbRuntime::definition( + const std::size_t index) const noexcept { + return initialized_ && index < definitions_.size() ? &definitions_[index] : nullptr; +} + +const MmsStaticUrcbState* MmsStaticUrcbRuntime::state( + const std::size_t index) const noexcept { + return initialized_ && index < definitions_.size() ? &states_[index] : nullptr; +} + +MmsStaticUrcbState* MmsStaticUrcbRuntime::state( + const std::size_t index) noexcept { + return initialized_ && index < definitions_.size() ? &states_[index] : nullptr; +} + +bool MmsStaticUrcbRuntime::find_index( + const MmsObjectNameView& name, + std::size_t& index) const noexcept { + index = 0U; + if (!initialized_ || name.kind != MmsObjectNameViewKind::domain_specific) { + return false; + } + for (std::size_t current = 0U; current < definitions_.size(); ++current) { + const auto& definition_ref = definitions_[current]; + if (name.domain.size() != definition_ref.domain.size() || + name.item.size() != definition_ref.item.size()) { + continue; + } + if (std::equal( + name.domain.begin(), + name.domain.end(), + reinterpret_cast(definition_ref.domain.data())) && + std::equal( + name.item.begin(), + name.item.end(), + reinterpret_cast(definition_ref.item.data()))) { + index = current; + return true; + } + } + return false; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_enabled( + const std::size_t index, + const bool enabled, + const std::uint64_t now_ms) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->enabled == enabled) { + return MmsStaticUrcbStatus::ok; + } + + state_ref->enabled = enabled; + state_ref->general_interrogation_pending = false; + if (enabled) { + state_ref->sequence_number = 0U; + refresh_integrity_arm(*state_ref, now_ms); + } else { + state_ref->integrity_armed = false; + state_ref->next_integrity_due_ms = 0U; + } + bump_revision(*state_ref); + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_reserved( + const std::size_t index, + const bool reserved) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->reserved != reserved) { + state_ref->reserved = reserved; + bump_revision(*state_ref); + } + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_report_id( + const std::size_t index, + const std::string_view report_id) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->enabled) { + return MmsStaticUrcbStatus::object_access_denied; + } + if (!valid_report_id(report_id)) { + return MmsStaticUrcbStatus::invalid_value; + } + if (state_ref->report_id() == report_id) { + return MmsStaticUrcbStatus::ok; + } + if (!copy_text( + report_id, + state_ref->report_id_storage, + state_ref->report_id_size)) { + return MmsStaticUrcbStatus::invalid_value; + } + bump_revision(*state_ref); + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_data_set( + const std::size_t index, + const std::string_view domain, + const std::string_view item) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->enabled) { + return MmsStaticUrcbStatus::object_access_denied; + } + if (!valid_reference(domain, item)) { + return MmsStaticUrcbStatus::invalid_value; + } + if (data_sets_->find(object_name(domain, item)) == nullptr) { + return MmsStaticUrcbStatus::data_set_not_found; + } + if (state_ref->data_set_domain() == domain && state_ref->data_set_item() == item) { + return MmsStaticUrcbStatus::ok; + } + if (!copy_text( + domain, + state_ref->data_set_domain_storage, + state_ref->data_set_domain_size) || + !copy_text( + item, + state_ref->data_set_item_storage, + state_ref->data_set_item_size)) { + return MmsStaticUrcbStatus::invalid_value; + } + bump_revision(*state_ref); + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_optional_fields( + const std::size_t index, + const std::span optional_fields) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->enabled) { + return MmsStaticUrcbStatus::object_access_denied; + } + if (!valid_optional_fields(optional_fields)) { + return MmsStaticUrcbStatus::invalid_value; + } + const std::array + next{optional_fields[0], optional_fields[1]}; + if (state_ref->optional_fields != next) { + state_ref->optional_fields = next; + bump_revision(*state_ref); + } + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_buffer_time_ms( + const std::size_t index, + const std::uint32_t buffer_time_ms) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->enabled) { + return MmsStaticUrcbStatus::object_access_denied; + } + if (state_ref->buffer_time_ms != buffer_time_ms) { + state_ref->buffer_time_ms = buffer_time_ms; + bump_revision(*state_ref); + } + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_trigger_options( + const std::size_t index, + const std::uint8_t trigger_options) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->enabled) { + return MmsStaticUrcbStatus::object_access_denied; + } + if (!valid_trigger_options(trigger_options)) { + return MmsStaticUrcbStatus::invalid_value; + } + if (state_ref->trigger_options != trigger_options) { + state_ref->trigger_options = trigger_options; + bump_revision(*state_ref); + } + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::set_integrity_period_ms( + const std::size_t index, + const std::uint32_t integrity_period_ms) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (state_ref->enabled) { + return MmsStaticUrcbStatus::object_access_denied; + } + if (state_ref->integrity_period_ms != integrity_period_ms) { + state_ref->integrity_period_ms = integrity_period_ms; + bump_revision(*state_ref); + } + return MmsStaticUrcbStatus::ok; +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::request_general_interrogation( + const std::size_t index) noexcept { + auto* state_ref = state(index); + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (state_ref == nullptr) { + return MmsStaticUrcbStatus::index_out_of_range; + } + if (!state_ref->enabled) { + return MmsStaticUrcbStatus::temporarily_unavailable; + } + if (!state_ref->general_interrogation_pending) { + state_ref->general_interrogation_pending = true; + bump_revision(*state_ref); + } + return MmsStaticUrcbStatus::ok; +} + +bool MmsStaticUrcbRuntime::next_due( + const std::uint64_t now_ms, + MmsStaticUrcbEmissionPlan& plan) const noexcept { + plan = {}; + if (!initialized_) { + return false; + } + + for (std::size_t index = 0U; index < definitions_.size(); ++index) { + const auto& state_ref = states_[index]; + if (state_ref.enabled && state_ref.general_interrogation_pending) { + plan.index = index; + plan.revision = state_ref.revision; + plan.sequence_number = next_sequence_number(state_ref.sequence_number); + plan.reason = MmsStaticUrcbReportReason::general_interrogation; + return true; + } + } + for (std::size_t index = 0U; index < definitions_.size(); ++index) { + const auto& state_ref = states_[index]; + if (state_ref.enabled && state_ref.integrity_armed && + now_ms >= state_ref.next_integrity_due_ms) { + plan.index = index; + plan.revision = state_ref.revision; + plan.sequence_number = next_sequence_number(state_ref.sequence_number); + plan.reason = MmsStaticUrcbReportReason::integrity; + return true; + } + } + return false; +} + +MmsStaticUrcbEncodeResult MmsStaticUrcbRuntime::encode( + const MmsStaticUrcbEmissionPlan& plan, + const std::span report_time, + const std::span destination, + const std::span workspace) const noexcept { + if (!initialized_ || objects_ == nullptr || data_sets_ == nullptr) { + return {MmsStaticUrcbStatus::invalid_runtime, 0U, 0U, 0U}; + } + if (plan.index >= definitions_.size()) { + return {MmsStaticUrcbStatus::index_out_of_range, 0U, 0U, 0U}; + } + const auto& state_ref = states_[plan.index]; + if (!plan_matches_state(plan, state_ref)) { + return {MmsStaticUrcbStatus::stale_plan, 0U, 0U, 0U}; + } + + const auto data_set_name = object_name( + state_ref.data_set_domain(), + state_ref.data_set_item()); + MmsStaticInformationReportInput report; + report.report_id = state_ref.report_id(); + report.optional_fields = state_ref.optional_fields; + report.sequence_number = plan.sequence_number; + report.report_time = report_time; + report.buffered = false; + report.buffer_overflow = false; + report.entry_id = {}; + report.conf_revision = state_ref.conf_revision; + report.reason_for_inclusion = static_cast(plan.reason); + + return map_report_result( + MmsStaticInformationReportEncoder::encode_data_set_snapshot_into( + *objects_, + *data_sets_, + data_set_name, + report, + destination, + workspace)); +} + +MmsStaticUrcbStatus MmsStaticUrcbRuntime::commit( + const MmsStaticUrcbEmissionPlan& plan, + const std::uint64_t now_ms) noexcept { + if (!initialized_) { + return MmsStaticUrcbStatus::invalid_runtime; + } + if (plan.index >= definitions_.size()) { + return MmsStaticUrcbStatus::index_out_of_range; + } + auto& state_ref = states_[plan.index]; + if (!plan_matches_state(plan, state_ref)) { + return MmsStaticUrcbStatus::stale_plan; + } + + state_ref.sequence_number = plan.sequence_number; + if (plan.reason == MmsStaticUrcbReportReason::general_interrogation) { + state_ref.general_interrogation_pending = false; + } else if (plan.reason == MmsStaticUrcbReportReason::integrity) { + const auto period = effective_integrity_period(state_ref); + state_ref.next_integrity_due_ms = period == 0U + ? 0U + : saturating_add(now_ms, period); + state_ref.integrity_armed = period != 0U && + (state_ref.trigger_options & kTriggerIntegrity) != 0U && state_ref.enabled; + } else { + return MmsStaticUrcbStatus::stale_plan; + } + bump_revision(state_ref); + return MmsStaticUrcbStatus::ok; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/static_write.cpp b/src/mms/static_write.cpp new file mode 100644 index 000000000..5ffa21614 --- /dev/null +++ b/src/mms/static_write.cpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_object_table.hpp" + +#include +#include + +namespace ar::iec61850::mms { + +bool MmsStaticObjectTable::try_resolve_write_request( + const MmsWriteRequestView& request, + const std::span resolved, + std::size_t& resolved_count) const noexcept { + resolved_count = 0U; + if (request.variable_count == 0U || + request.variable_count != request.data_count || + request.variable_count > resolved.size() || + request.variable_count > MmsServiceSpanCodec::maximum_variables) { + return false; + } + for (std::size_t index = 0U; index < request.variable_count; ++index) { + MmsObjectNameView name; + if (!request.try_variable(index, name)) { + resolved_count = 0U; + return false; + } + resolved[index] = find(name); + ++resolved_count; + } + return true; +} + +} // namespace ar::iec61850::mms diff --git a/src/mms/utc_time.cpp b/src/mms/utc_time.cpp index 0abb34c5a..67439e4f7 100644 --- a/src/mms/utc_time.cpp +++ b/src/mms/utc_time.cpp @@ -7,13 +7,18 @@ #include #include #include +#if !defined(ARIEC61850_NO_EXCEPTIONS) #include +#endif namespace ar::iec61850::mms { -Iec61850UtcTime Iec61850UtcTime::from_bytes(const std::span bytes) { +bool Iec61850UtcTime::try_from_bytes( + const std::span bytes, + Iec61850UtcTime& result) noexcept { if (bytes.size() != 8U) { - throw std::invalid_argument("IEC 61850 UTC time requires exactly 8 bytes."); + result = {}; + return false; } const auto seconds = @@ -30,8 +35,19 @@ Iec61850UtcTime Iec61850UtcTime::from_bytes(const std::span const auto timestamp = std::chrono::system_clock::time_point{std::chrono::seconds{seconds}} + std::chrono::duration_cast( fraction_duration{fraction}); - return {timestamp, bytes[7]}; + result = {timestamp, bytes[7]}; + return true; +} + +#if !defined(ARIEC61850_NO_EXCEPTIONS) +Iec61850UtcTime Iec61850UtcTime::from_bytes(const std::span bytes) { + Iec61850UtcTime result{}; + if (!try_from_bytes(bytes, result)) { + throw std::invalid_argument("IEC 61850 UTC time requires exactly 8 bytes."); + } + return result; } +#endif bool Iec61850UtcTime::try_write_bytes( const std::span destination) const noexcept { @@ -64,6 +80,7 @@ bool Iec61850UtcTime::try_write_bytes( return true; } +#if !defined(ARIEC61850_NO_EXCEPTIONS) std::vector Iec61850UtcTime::to_bytes() const { std::array bytes{}; if (!try_write_bytes(bytes)) { @@ -71,5 +88,6 @@ std::vector Iec61850UtcTime::to_bytes() const { } return {bytes.begin(), bytes.end()}; } +#endif } // namespace ar::iec61850::mms diff --git a/src/mms/write_span.cpp b/src/mms/write_span.cpp new file mode 100644 index 000000000..1871f0852 --- /dev/null +++ b/src/mms/write_span.cpp @@ -0,0 +1,458 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/services_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" + +#include +#include +#include +#include +#include + +namespace ar::iec61850::mms { +namespace { + +[[nodiscard]] bool read_u32( + const asn1::BerTlvView& tlv, + std::uint32_t& value) noexcept { + const auto decoded = asn1::BerSpanReader::read_uint32(tlv); + if (!decoded) { + value = 0U; + return false; + } + value = *decoded; + return true; +} + +[[nodiscard]] bool valid_mms_data_tlv( + const asn1::BerTlvView& tlv) noexcept { + if (tlv.tag_class != asn1::BerClass::context_specific) { + return false; + } + if (tlv.tag_number == 1 || tlv.tag_number == 2) { + return tlv.constructed; + } + return tlv.tag_number >= 3 && tlv.tag_number <= 17 && !tlv.constructed; +} + +[[nodiscard]] std::size_t minimal_unsigned_size(std::uint32_t value) noexcept { + std::size_t bytes = 1U; + while (value > 0xFFU) { + ++bytes; + value >>= 8U; + } + return bytes; +} + +[[nodiscard]] bool needs_positive_prefix( + const std::uint32_t value, + const std::size_t minimal_bytes) noexcept { + const auto shift = static_cast((minimal_bytes - 1U) * 8U); + return ((value >> shift) & 0x80U) != 0U; +} + +[[nodiscard]] std::size_t positive_integer_size(const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + return minimal + (needs_positive_prefix(value, minimal) ? 1U : 0U); +} + +[[nodiscard]] bool write_positive_integer( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto minimal = minimal_unsigned_size(value); + if (needs_positive_prefix(value, minimal) && !writer.write_byte(0x00U)) { + return false; + } + for (std::size_t index = minimal; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast( + (value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] std::optional add_size( + const std::optional left, + const std::optional right) noexcept { + if (!left || !right || *right > std::numeric_limits::max() - *left) { + return std::nullopt; + } + return *left + *right; +} + +struct ConfirmedResponseSizing final { + std::size_t invoke_value{}; + std::size_t outer_content{}; + std::size_t required{}; +}; + +[[nodiscard]] std::optional confirmed_response_sizing( + const std::uint32_t invoke_id, + const std::size_t service_content) noexcept { + if (invoke_id > MmsPduSpanCodec::maximum_invoke_id) { + return std::nullopt; + } + const auto invoke_value = positive_integer_size(invoke_id); + const auto invoke_tlv = asn1::BerSpanWriter::tlv_size(2, invoke_value); + const auto service_tlv = asn1::BerSpanWriter::tlv_size(5, service_content); + const auto outer_content = add_size(invoke_tlv, service_tlv); + if (!outer_content) { + return std::nullopt; + } + const auto required = asn1::BerSpanWriter::tlv_size(1, *outer_content); + if (!required || *required > MmsPduSpanCodec::maximum_pdu_bytes) { + return std::nullopt; + } + return ConfirmedResponseSizing{invoke_value, *outer_content, *required}; +} + +[[nodiscard]] bool write_confirmed_response_prefix( + asn1::BerSpanWriter& writer, + const ConfirmedResponseSizing& sizing, + const std::uint32_t invoke_id, + const std::size_t service_content) noexcept { + return writer.write_tlv_header( + asn1::BerClass::context_specific, true, 1, sizing.outer_content) && + writer.write_tlv_header( + asn1::BerClass::universal, false, 2, sizing.invoke_value) && + write_positive_integer(writer, invoke_id) && + writer.write_tlv_header( + asn1::BerClass::context_specific, true, 5, service_content); +} + +[[nodiscard]] bool try_read_variable_definition( + const std::span variable_list, + const std::size_t wanted, + MmsObjectNameView& name) noexcept { + name = {}; + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < variable_list.size()) { + asn1::BerTlvView definition; + if (!asn1::BerSpanReader::try_read_tlv(variable_list, offset, definition) || + definition.tag_class != asn1::BerClass::universal || + definition.tag_number != 16 || !definition.constructed) { + return false; + } + asn1::BerTlvView wrapper; + if (!asn1::BerSpanReader::try_read_exact(definition.value, wrapper) || + wrapper.tag_class != asn1::BerClass::context_specific || + wrapper.tag_number != 0 || !wrapper.constructed) { + return false; + } + MmsObjectNameView decoded; + if (!MmsServiceSpanCodec::try_decode_object_name_view(wrapper.value, decoded)) { + return false; + } + if (current == wanted) { + name = decoded; + return true; + } + ++current; + } + return false; +} + +[[nodiscard]] bool try_read_data_value( + const std::span data_list, + const std::size_t wanted, + std::span& encoded_data) noexcept { + encoded_data = {}; + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < data_list.size()) { + const auto start = offset; + asn1::BerTlvView value; + if (!asn1::BerSpanReader::try_read_tlv(data_list, offset, value) || + !valid_mms_data_tlv(value)) { + return false; + } + if (current == wanted) { + encoded_data = data_list.subspan(start, offset - start); + return true; + } + ++current; + } + return false; +} + +[[nodiscard]] bool try_read_write_result( + const std::span result_list, + const std::size_t wanted, + MmsWriteAccessResultView& result) noexcept { + result = {}; + std::size_t offset = 0U; + std::size_t current = 0U; + while (offset < result_list.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(result_list, offset, field) || + field.tag_class != asn1::BerClass::context_specific || field.constructed) { + return false; + } + MmsWriteAccessResultView decoded; + if (field.tag_number == 1) { + if (!field.value.empty()) { + return false; + } + decoded.success = true; + } else if (field.tag_number == 0) { + if (!read_u32(field, decoded.failure_code)) { + return false; + } + decoded.success = false; + } else { + return false; + } + if (current == wanted) { + result = decoded; + return true; + } + ++current; + } + return false; +} + +} // namespace + +bool MmsWriteRequestView::try_variable( + const std::size_t index, + MmsObjectNameView& name) const noexcept { + if (index >= variable_count) { + name = {}; + return false; + } + return try_read_variable_definition(variable_list, index, name); +} + +bool MmsWriteRequestView::try_value( + const std::size_t index, + std::span& encoded_data) const noexcept { + if (index >= data_count) { + encoded_data = {}; + return false; + } + return try_read_data_value(data_list, index, encoded_data); +} + +bool MmsWriteResponseView::try_result( + const std::size_t index, + MmsWriteAccessResultView& result) const noexcept { + if (implicit_single_success) { + if (index != 0U) { + result = {}; + return false; + } + result = MmsWriteAccessResultView{true, 0U}; + return true; + } + if (index >= result_count) { + result = {}; + return false; + } + return try_read_write_result(result_list, index, result); +} + +bool MmsServiceSpanCodec::try_decode_write_request( + const MmsConfirmedPduView& confirmed, + MmsWriteRequestView& request) noexcept { + request = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_request || + confirmed.service_tag != 5 || !confirmed.service_constructed) { + return false; + } + + std::size_t offset = 0U; + asn1::BerTlvView variables; + asn1::BerTlvView data; + if (!asn1::BerSpanReader::try_read_tlv(confirmed.service_value, offset, variables) || + !asn1::BerSpanReader::try_read_tlv(confirmed.service_value, offset, data) || + offset != confirmed.service_value.size() || + variables.tag_class != asn1::BerClass::context_specific || + variables.tag_number != 0 || !variables.constructed || + data.tag_class != asn1::BerClass::context_specific || + data.tag_number != 0 || !data.constructed) { + return false; + } + + std::size_t variable_offset = 0U; + std::size_t variable_count = 0U; + while (variable_offset < variables.value.size()) { + if (variable_count >= maximum_variables) { + return false; + } + const auto before = variable_offset; + asn1::BerTlvView definition; + if (!asn1::BerSpanReader::try_read_tlv( + variables.value, variable_offset, definition)) { + return false; + } + MmsObjectNameView ignored; + if (!try_read_variable_definition( + variables.value.subspan(before, variable_offset - before), 0U, ignored)) { + return false; + } + ++variable_count; + } + + std::size_t data_offset = 0U; + std::size_t data_count = 0U; + while (data_offset < data.value.size()) { + if (data_count >= maximum_variables) { + return false; + } + asn1::BerTlvView value; + if (!asn1::BerSpanReader::try_read_tlv(data.value, data_offset, value) || + !valid_mms_data_tlv(value)) { + return false; + } + ++data_count; + } + + if (variable_count == 0U || variable_count != data_count) { + return false; + } + request.invoke_id = confirmed.invoke_id; + request.variable_list = variables.value; + request.variable_count = variable_count; + request.data_list = data.value; + request.data_count = data_count; + return true; +} + +bool MmsServiceSpanCodec::try_decode_write_request( + const std::span mms_pdu, + MmsWriteRequestView& request) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_request_view(mms_pdu, confirmed) && + try_decode_write_request(confirmed, request); +} + +bool MmsServiceSpanCodec::try_decode_write_response( + const MmsConfirmedPduView& confirmed, + MmsWriteResponseView& response) noexcept { + response = {}; + if (confirmed.kind != MmsWirePduKind::confirmed_response || + confirmed.service_tag != 5) { + return false; + } + response.invoke_id = confirmed.invoke_id; + if (!confirmed.service_constructed) { + if (!confirmed.service_value.empty()) { + response = {}; + return false; + } + response.implicit_single_success = true; + response.result_count = 1U; + return true; + } + + std::size_t offset = 0U; + std::size_t count = 0U; + while (offset < confirmed.service_value.size()) { + if (count >= maximum_variables) { + response = {}; + return false; + } + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv( + confirmed.service_value, offset, field) || + field.tag_class != asn1::BerClass::context_specific || field.constructed) { + response = {}; + return false; + } + if (field.tag_number == 1) { + if (!field.value.empty()) { + response = {}; + return false; + } + } else if (field.tag_number == 0) { + std::uint32_t ignored{}; + if (!read_u32(field, ignored)) { + response = {}; + return false; + } + } else { + response = {}; + return false; + } + ++count; + } + if (count == 0U) { + response = {}; + return false; + } + response.result_list = confirmed.service_value; + response.result_count = count; + return true; +} + +bool MmsServiceSpanCodec::try_decode_write_response( + const std::span mms_pdu, + MmsWriteResponseView& response) noexcept { + MmsConfirmedPduView confirmed; + return MmsPduSpanCodec::try_decode_confirmed_response_view(mms_pdu, confirmed) && + try_decode_write_response(confirmed, response); +} + +wire::EncodeResult MmsServiceSpanCodec::encode_write_response_into( + const std::uint32_t invoke_id, + const std::span results, + const std::span destination) noexcept { + if (results.empty() || results.size() > maximum_variables) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + std::size_t service_content = 0U; + for (const auto& result : results) { + std::optional encoded; + if (result.success) { + encoded = asn1::BerSpanWriter::tlv_size(1, 0U); + } else { + encoded = asn1::BerSpanWriter::tlv_size( + 0, positive_integer_size(result.failure_code)); + } + if (!encoded || *encoded > std::numeric_limits::max() - service_content) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + service_content += *encoded; + } + + const auto sizing = confirmed_response_sizing(invoke_id, service_content); + if (!sizing) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < sizing->required) { + return {wire::EncodeStatus::buffer_too_small, 0U, sizing->required}; + } + + asn1::BerSpanWriter writer{destination.first(sizing->required)}; + if (!write_confirmed_response_prefix( + writer, *sizing, invoke_id, service_content)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + for (const auto& result : results) { + if (result.success) { + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 1, 0U)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + } else { + const auto failure_value = positive_integer_size(result.failure_code); + if (!writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, failure_value) || + !write_positive_integer(writer, result.failure_code)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + } + } + if (writer.size() != sizing->required) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizing->required}; + } + return {wire::EncodeStatus::ok, sizing->required, sizing->required}; +} + +} // namespace ar::iec61850::mms diff --git a/src/osi/cotp_span.cpp b/src/osi/cotp_span.cpp new file mode 100644 index 000000000..56cdd5375 --- /dev/null +++ b/src/osi/cotp_span.cpp @@ -0,0 +1,432 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/osi/cotp_span.hpp" + +#include +#include +#include +#include +#include + +namespace ar::iec61850::osi { +namespace { + +[[nodiscard]] std::uint16_t read_be_u16( + const std::span bytes, + const std::size_t offset) noexcept { + return static_cast( + (static_cast(bytes[offset]) << 8U) | + static_cast(bytes[offset + 1U])); +} + +void write_be_u16( + const std::span bytes, + const std::size_t offset, + const std::uint16_t value) noexcept { + bytes[offset] = static_cast((value >> 8U) & 0xFFU); + bytes[offset + 1U] = static_cast(value & 0xFFU); +} + +[[nodiscard]] bool encoded_parameters_size( + const std::span parameters, + std::size_t& total) noexcept { + total = 0U; + for (const auto& parameter : parameters) { + if (parameter.value.size() > 248U) { + total = 0U; + return false; + } + const auto encoded = 2U + parameter.value.size(); + if (encoded > 248U - total) { + total = 0U; + return false; + } + total += encoded; + } + return true; +} + +[[nodiscard]] bool write_parameter( + const std::span destination, + std::size_t& offset, + const std::uint8_t code, + const std::span value) noexcept { + if (value.size() > 248U || destination.size() - offset < value.size() + 2U) { + return false; + } + destination[offset++] = code; + destination[offset++] = static_cast(value.size()); + std::copy( + value.begin(), + value.end(), + destination.begin() + static_cast(offset)); + offset += value.size(); + return true; +} + +[[nodiscard]] wire::EncodeResult encode_reference_into( + const std::uint8_t code, + const std::uint16_t destination_reference, + const std::uint16_t source_reference, + const std::uint8_t class_or_reason, + const std::span parameters, + const std::span destination) noexcept { + std::size_t parameter_bytes{}; + if (!encoded_parameters_size(parameters, parameter_bytes)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto length_indicator = 6U + parameter_bytes; + const auto required = length_indicator + 1U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + destination[0] = static_cast(length_indicator); + destination[1] = code; + write_be_u16(destination, 2U, destination_reference); + write_be_u16(destination, 4U, source_reference); + destination[6] = class_or_reason; + + std::size_t offset = 7U; + for (const auto& parameter : parameters) { + if (!write_parameter(destination, offset, parameter.code, parameter.value)) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + } + if (offset != required) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] bool validate_parameter_bytes( + const std::span bytes) noexcept { + std::size_t offset = 0U; + while (offset < bytes.size()) { + if (bytes.size() - offset < 2U) { + return false; + } + const auto length = static_cast(bytes[offset + 1U]); + offset += 2U; + if (length > bytes.size() - offset) { + return false; + } + offset += length; + } + return offset == bytes.size(); +} + +[[nodiscard]] bool selected_tpdu_size( + const CotpTpduView& request, + const std::uint8_t maximum_tpdu_size_code, + std::uint8_t& selected) noexcept { + std::size_t ignored{}; + if (!CotpSpanCodec::try_tpdu_size_bytes(maximum_tpdu_size_code, ignored)) { + return false; + } + + std::span offered; + if (!request.try_parameter(CotpSpanCodec::tpdu_size_parameter, offered) || + offered.size() != 1U) { + selected = maximum_tpdu_size_code; + return true; + } + + if (!CotpSpanCodec::try_tpdu_size_bytes(offered[0], ignored)) { + return false; + } + selected = std::min(offered[0], maximum_tpdu_size_code); + return true; +} + +[[nodiscard]] bool mirrored_parameter_size( + const CotpTpduView& request, + std::size_t& total) noexcept { + // The CC always starts with C0 / TPDU size (3 encoded bytes). + total = 3U; + std::size_t offset = 0U; + while (offset < request.parameter_bytes.size()) { + if (request.parameter_bytes.size() - offset < 2U) { + total = 0U; + return false; + } + const auto code = request.parameter_bytes[offset]; + const auto length = static_cast(request.parameter_bytes[offset + 1U]); + offset += 2U; + if (length > request.parameter_bytes.size() - offset) { + total = 0U; + return false; + } + + if ((code == CotpSpanCodec::source_tsap_parameter || + code == CotpSpanCodec::destination_tsap_parameter) && + length != 0U) { + const auto encoded = length + 2U; + if (length > 248U || encoded > 248U - total) { + total = 0U; + return false; + } + total += encoded; + } + offset += length; + } + return true; +} + +} // namespace + +bool CotpTpduView::try_parameter( + const std::uint8_t parameter_code, + std::span& value) const noexcept { + value = {}; + std::size_t offset = 0U; + while (offset < parameter_bytes.size()) { + if (parameter_bytes.size() - offset < 2U) { + return false; + } + const auto current_code = parameter_bytes[offset]; + const auto length = static_cast(parameter_bytes[offset + 1U]); + offset += 2U; + if (length > parameter_bytes.size() - offset) { + return false; + } + if (current_code == parameter_code) { + value = parameter_bytes.subspan(offset, length); + return true; + } + offset += length; + } + return false; +} + +CotpWireKind CotpSpanCodec::kind_from_code(const std::uint8_t code) noexcept { + switch (code) { + case connection_request_code: + return CotpWireKind::connection_request; + case connection_confirm_code: + return CotpWireKind::connection_confirm; + case data_code: + return CotpWireKind::data; + case disconnect_request_code: + return CotpWireKind::disconnect_request; + case error_code: + return CotpWireKind::error; + default: + return CotpWireKind::unknown; + } +} + +bool CotpSpanCodec::try_decode_view( + const std::span bytes, + CotpTpduView& tpdu) noexcept { + tpdu = {}; + if (bytes.size() < 2U) { + return false; + } + + const auto length_indicator = static_cast(bytes[0]); + const auto header_end = length_indicator + 1U; + if (length_indicator < 2U || header_end > bytes.size()) { + return false; + } + + const auto kind = kind_from_code(bytes[1]); + if (kind == CotpWireKind::unknown) { + return false; + } + + tpdu.code = bytes[1]; + tpdu.kind = kind; + tpdu.length_indicator = bytes[0]; + + if (kind == CotpWireKind::data) { + if (length_indicator != 2U || header_end != 3U) { + tpdu = {}; + return false; + } + tpdu.end_of_transmission = (bytes[2] & 0x80U) != 0U; + tpdu.tpdu_number = static_cast(bytes[2] & 0x7FU); + tpdu.user_data = bytes.subspan(3U); + return true; + } + + if (length_indicator < 6U || bytes.size() < 7U) { + tpdu = {}; + return false; + } + + tpdu.destination_reference = read_be_u16(bytes, 2U); + tpdu.source_reference = read_be_u16(bytes, 4U); + tpdu.class_or_reason = bytes[6]; + tpdu.end_of_transmission = true; + tpdu.parameter_bytes = bytes.subspan(7U, header_end - 7U); + tpdu.user_data = bytes.subspan(header_end); + + if (!validate_parameter_bytes(tpdu.parameter_bytes) || + ((kind == CotpWireKind::connection_request || + kind == CotpWireKind::connection_confirm) && + !tpdu.user_data.empty())) { + tpdu = {}; + return false; + } + return true; +} + +bool CotpSpanCodec::try_tpdu_size_bytes( + const std::uint8_t tpdu_size_code, + std::size_t& bytes) noexcept { + bytes = 0U; + if (tpdu_size_code < 0x07U || tpdu_size_code > 0x0FU) { + return false; + } + bytes = static_cast(1U) << tpdu_size_code; + return true; +} + +wire::EncodeResult CotpSpanCodec::encode_data_into( + const std::span user_data, + const std::span destination, + const bool end_of_transmission, + const std::uint8_t tpdu_number) noexcept { + if (tpdu_number > 0x7FU || + user_data.size() > std::numeric_limits::max() - 3U) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto required = user_data.size() + 3U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + destination[0] = 0x02U; + destination[1] = data_code; + destination[2] = static_cast( + (end_of_transmission ? 0x80U : 0x00U) | tpdu_number); + std::copy( + user_data.begin(), + user_data.end(), + destination.begin() + 3); + return {wire::EncodeStatus::ok, required, required}; +} + +wire::EncodeResult CotpSpanCodec::encode_connection_request_into( + const std::uint16_t source_reference, + const std::span parameters, + const std::span destination, + const std::uint16_t destination_reference, + const std::uint8_t transport_class) noexcept { + return encode_reference_into( + connection_request_code, + destination_reference, + source_reference, + transport_class, + parameters, + destination); +} + +wire::EncodeResult CotpSpanCodec::encode_connection_confirm_into( + const std::uint16_t destination_reference, + const std::uint16_t source_reference, + const std::span destination, + const std::uint8_t maximum_tpdu_size_code) noexcept { + std::size_t ignored{}; + if (!try_tpdu_size_bytes(maximum_tpdu_size_code, ignored)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const std::uint8_t value = maximum_tpdu_size_code; + const CotpParameterView parameter{ + tpdu_size_parameter, + std::span{&value, 1U}}; + return encode_reference_into( + connection_confirm_code, + destination_reference, + source_reference, + 0U, + std::span{¶meter, 1U}, + destination); +} + +wire::EncodeResult CotpSpanCodec::encode_connection_confirm_from_request_into( + const CotpTpduView& connection_request, + const std::uint16_t source_reference, + const std::span destination, + const std::uint8_t maximum_tpdu_size_code) noexcept { + if (connection_request.kind != CotpWireKind::connection_request || + !connection_request.user_data.empty()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + std::uint8_t selected{}; + std::size_t parameter_bytes{}; + if (!selected_tpdu_size( + connection_request, maximum_tpdu_size_code, selected) || + !mirrored_parameter_size(connection_request, parameter_bytes)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto length_indicator = 6U + parameter_bytes; + const auto required = length_indicator + 1U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + destination[0] = static_cast(length_indicator); + destination[1] = connection_confirm_code; + write_be_u16(destination, 2U, connection_request.source_reference); + write_be_u16(destination, 4U, source_reference); + destination[6] = 0U; + + std::size_t output_offset = 7U; + const std::span selected_span{&selected, 1U}; + if (!write_parameter( + destination, + output_offset, + tpdu_size_parameter, + selected_span)) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + + std::size_t input_offset = 0U; + while (input_offset < connection_request.parameter_bytes.size()) { + if (connection_request.parameter_bytes.size() - input_offset < 2U) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + const auto code = connection_request.parameter_bytes[input_offset]; + const auto length = static_cast( + connection_request.parameter_bytes[input_offset + 1U]); + input_offset += 2U; + if (length > connection_request.parameter_bytes.size() - input_offset) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + const auto value = connection_request.parameter_bytes.subspan( + input_offset, length); + + if (code == source_tsap_parameter && !value.empty()) { + if (!write_parameter( + destination, + output_offset, + destination_tsap_parameter, + value)) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + } else if (code == destination_tsap_parameter && !value.empty()) { + if (!write_parameter( + destination, + output_offset, + source_tsap_parameter, + value)) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + } + input_offset += length; + } + + if (output_offset != required) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + return {wire::EncodeStatus::ok, required, required}; +} + +} // namespace ar::iec61850::osi diff --git a/src/osi/presentation_span.cpp b/src/osi/presentation_span.cpp new file mode 100644 index 000000000..b0d535793 --- /dev/null +++ b/src/osi/presentation_span.cpp @@ -0,0 +1,728 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/osi/presentation_span.hpp" + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/asn1/ber_span_writer.hpp" +#include "ariec61850/osi/session_span.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::osi { +namespace { + +constexpr std::array kAcseAbstractSyntax{ + 0x52U, 0x01U, 0x00U, 0x01U}; +constexpr std::array kMmsAbstractSyntax{ + 0x28U, 0xCAU, 0x22U, 0x02U, 0x01U}; +constexpr std::array kBerTransferSyntax{0x51U, 0x01U}; + +[[nodiscard]] std::size_t unsigned_integer_size(std::uint32_t value) noexcept { + std::size_t size = 1U; + while (value > 0xFFU) { + ++size; + value >>= 8U; + } + return size; +} + +[[nodiscard]] bool write_unsigned_integer( + asn1::BerSpanWriter& writer, + const std::uint32_t value) noexcept { + const auto size = unsigned_integer_size(value); + for (std::size_t index = size; index-- > 0U;) { + const auto shift = static_cast(index * 8U); + if (!writer.write_byte(static_cast( + (value >> shift) & 0xFFU))) { + return false; + } + } + return true; +} + +[[nodiscard]] std::optional add_size( + const std::optional left, + const std::optional right) noexcept { + if (!left || !right || *right > std::numeric_limits::max() - *left) { + return std::nullopt; + } + return *left + *right; +} + +[[nodiscard]] bool valid_oid( + const std::span value) noexcept { + return !value.empty() && value.size() <= PresentationSpanCodec::maximum_oid_bytes; +} + +[[nodiscard]] bool read_positive_integer( + const asn1::BerTlvView& tlv, + std::uint32_t& value) noexcept { + const auto decoded = asn1::BerSpanReader::read_uint32(tlv); + if (!decoded || *decoded == 0U) { + value = 0U; + return false; + } + value = *decoded; + return true; +} + +struct ContextDefinitionView final { + std::uint32_t id{}; + std::span abstract_syntax{}; + std::span first_transfer_syntax{}; + std::size_t transfer_syntax_count{}; +}; + +[[nodiscard]] bool parse_context_definition( + const asn1::BerTlvView& definition, + ContextDefinitionView& context) noexcept { + context = {}; + if (definition.tag_class != asn1::BerClass::universal || + definition.tag_number != 16 || !definition.constructed) { + return false; + } + + bool saw_id = false; + bool saw_abstract = false; + bool saw_transfers = false; + std::size_t offset = 0U; + while (offset < definition.value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(definition.value, offset, field)) { + return false; + } + + if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 2 && !field.constructed) { + if (saw_id || !read_positive_integer(field, context.id)) { + return false; + } + saw_id = true; + } else if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 6 && !field.constructed) { + if (saw_abstract || !valid_oid(field.value)) { + return false; + } + context.abstract_syntax = field.value; + saw_abstract = true; + } else if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 16 && field.constructed) { + if (saw_transfers) { + return false; + } + std::size_t transfer_offset = 0U; + while (transfer_offset < field.value.size()) { + if (context.transfer_syntax_count >= + PresentationSpanCodec::maximum_contexts) { + return false; + } + asn1::BerTlvView syntax; + if (!asn1::BerSpanReader::try_read_tlv( + field.value, transfer_offset, syntax) || + syntax.tag_class != asn1::BerClass::universal || + syntax.tag_number != 6 || syntax.constructed || + !valid_oid(syntax.value)) { + return false; + } + if (context.transfer_syntax_count == 0U) { + context.first_transfer_syntax = syntax.value; + } + ++context.transfer_syntax_count; + } + if (context.transfer_syntax_count == 0U) { + return false; + } + saw_transfers = true; + } else { + return false; + } + } + return saw_id && saw_abstract && saw_transfers; +} + +[[nodiscard]] bool validate_context_list( + const std::span list, + std::size_t& count) noexcept { + count = 0U; + std::array ids{}; + std::size_t offset = 0U; + while (offset < list.size()) { + if (count >= PresentationSpanCodec::maximum_contexts) { + count = 0U; + return false; + } + asn1::BerTlvView definition; + ContextDefinitionView context; + if (!asn1::BerSpanReader::try_read_tlv(list, offset, definition) || + !parse_context_definition(definition, context)) { + count = 0U; + return false; + } + for (std::size_t index = 0U; index < count; ++index) { + if (ids[index] == context.id) { + count = 0U; + return false; + } + } + ids[count++] = context.id; + } + return count != 0U; +} + +[[nodiscard]] bool find_context_by_abstract_syntax( + const std::span list, + const std::span oid, + std::uint32_t& id) noexcept { + id = 0U; + std::size_t offset = 0U; + while (offset < list.size()) { + asn1::BerTlvView definition; + ContextDefinitionView context; + if (!asn1::BerSpanReader::try_read_tlv(list, offset, definition) || + !parse_context_definition(definition, context)) { + return false; + } + if (context.abstract_syntax.size() == oid.size() && + std::equal( + context.abstract_syntax.begin(), + context.abstract_syntax.end(), + oid.begin())) { + id = context.id; + return true; + } + } + return false; +} + +[[nodiscard]] bool context_id_exists( + const std::span list, + const std::uint32_t id) noexcept { + std::size_t offset = 0U; + while (offset < list.size()) { + asn1::BerTlvView definition; + ContextDefinitionView context; + if (!asn1::BerSpanReader::try_read_tlv(list, offset, definition) || + !parse_context_definition(definition, context)) { + return false; + } + if (context.id == id) { + return true; + } + } + return false; +} + +struct FullyEncodedSizes final { + std::size_t id_value{}; + std::size_t id_tlv{}; + std::size_t payload_tlv{}; + std::size_t sequence_content{}; + std::size_t sequence_tlv{}; + std::size_t outer_tlv{}; +}; + +[[nodiscard]] std::optional calculate_fully_encoded_sizes( + const std::uint32_t context_id, + const std::size_t payload_bytes) noexcept { + if (context_id == 0U || payload_bytes > PresentationSpanCodec::maximum_ppdu_bytes) { + return std::nullopt; + } + + FullyEncodedSizes sizes; + sizes.id_value = unsigned_integer_size(context_id); + const auto id_tlv = asn1::BerSpanWriter::tlv_size(2, sizes.id_value); + const auto payload_tlv = asn1::BerSpanWriter::tlv_size(0, payload_bytes); + const auto sequence_content = add_size(id_tlv, payload_tlv); + if (!id_tlv || !payload_tlv || !sequence_content) { + return std::nullopt; + } + const auto sequence_tlv = asn1::BerSpanWriter::tlv_size(16, *sequence_content); + if (!sequence_tlv) { + return std::nullopt; + } + const auto outer_tlv = asn1::BerSpanWriter::tlv_size(1, *sequence_tlv); + if (!outer_tlv || *outer_tlv > PresentationSpanCodec::maximum_ppdu_bytes) { + return std::nullopt; + } + + sizes.id_tlv = *id_tlv; + sizes.payload_tlv = *payload_tlv; + sizes.sequence_content = *sequence_content; + sizes.sequence_tlv = *sequence_tlv; + sizes.outer_tlv = *outer_tlv; + return sizes; +} + +[[nodiscard]] bool decode_fully_encoded_outer( + const asn1::BerTlvView& outer, + PresentationPdvView& pdv) noexcept { + pdv = {}; + if (outer.tag_class != asn1::BerClass::application || + outer.tag_number != 1 || !outer.constructed) { + return false; + } + + asn1::BerTlvView sequence; + if (!asn1::BerSpanReader::try_read_exact(outer.value, sequence) || + sequence.tag_class != asn1::BerClass::universal || + sequence.tag_number != 16 || !sequence.constructed) { + return false; + } + + bool saw_context_id = false; + bool saw_payload = false; + std::size_t offset = 0U; + while (offset < sequence.value.size()) { + asn1::BerTlvView field; + if (!asn1::BerSpanReader::try_read_tlv(sequence.value, offset, field)) { + pdv = {}; + return false; + } + + if (field.tag_class == asn1::BerClass::universal && + field.tag_number == 2 && !field.constructed) { + if (saw_context_id || !read_positive_integer(field, pdv.context_id)) { + pdv = {}; + return false; + } + saw_context_id = true; + } else if (field.tag_class == asn1::BerClass::context_specific && + field.tag_number == 0 && field.constructed) { + if (saw_payload) { + pdv = {}; + return false; + } + pdv.single_asn1_type = field.value; + saw_payload = true; + } else { + pdv = {}; + return false; + } + } + return saw_context_id && saw_payload; +} + +[[nodiscard]] bool decode_mode_selector( + const asn1::BerTlvView& item, + std::uint32_t& mode) noexcept { + if (item.tag_class != asn1::BerClass::context_specific || + item.tag_number != 0 || !item.constructed) { + return false; + } + asn1::BerTlvView value; + return asn1::BerSpanReader::try_read_exact(item.value, value) && + value.tag_class == asn1::BerClass::context_specific && + value.tag_number == 0 && !value.constructed && + read_positive_integer(value, mode); +} + +[[nodiscard]] std::optional context_results_content_size( + const PresentationCpView& request) noexcept { + std::size_t total = 0U; + std::size_t offset = 0U; + while (offset < request.context_definition_list.size()) { + asn1::BerTlvView definition; + ContextDefinitionView context; + if (!asn1::BerSpanReader::try_read_tlv( + request.context_definition_list, offset, definition) || + !parse_context_definition(definition, context)) { + return std::nullopt; + } + const auto result_tlv = asn1::BerSpanWriter::tlv_size(0, 1U); + const auto syntax_tlv = asn1::BerSpanWriter::tlv_size( + 1, context.first_transfer_syntax.size()); + const auto sequence_content = add_size(result_tlv, syntax_tlv); + if (!sequence_content) { + return std::nullopt; + } + const auto sequence_tlv = asn1::BerSpanWriter::tlv_size( + 16, *sequence_content); + if (!sequence_tlv || *sequence_tlv > + std::numeric_limits::max() - total) { + return std::nullopt; + } + total += *sequence_tlv; + } + return total; +} + +struct CpaSizes final { + std::size_t mode_value{}; + std::size_t mode_inner_tlv{}; + std::size_t mode_outer_tlv{}; + std::size_t results_content{}; + std::size_t results_tlv{}; + std::size_t user_tlv{}; + std::size_t normal_content{}; + std::size_t normal_tlv{}; + std::size_t outer_content{}; + std::size_t outer_tlv{}; +}; + +[[nodiscard]] std::optional calculate_cpa_sizes( + const PresentationCpView& request, + const std::size_t acse_aare_bytes) noexcept { + if (request.mode_selector == 0U || request.context_count == 0U) { + return std::nullopt; + } + + CpaSizes sizes; + sizes.mode_value = unsigned_integer_size(request.mode_selector); + const auto mode_inner = asn1::BerSpanWriter::tlv_size(0, sizes.mode_value); + if (!mode_inner) { + return std::nullopt; + } + const auto mode_outer = asn1::BerSpanWriter::tlv_size(0, *mode_inner); + const auto results_content = context_results_content_size(request); + if (!mode_outer || !results_content) { + return std::nullopt; + } + const auto results_tlv = asn1::BerSpanWriter::tlv_size(5, *results_content); + const auto user_tlv = PresentationSpanCodec::fully_encoded_data_size( + request.user_data.context_id, acse_aare_bytes); + const auto normal_content = add_size(results_tlv, user_tlv); + if (!results_tlv || !user_tlv || !normal_content) { + return std::nullopt; + } + const auto normal_tlv = asn1::BerSpanWriter::tlv_size(2, *normal_content); + const auto outer_content = add_size(mode_outer, normal_tlv); + if (!normal_tlv || !outer_content) { + return std::nullopt; + } + const auto outer_tlv = asn1::BerSpanWriter::tlv_size(17, *outer_content); + if (!outer_tlv || *outer_tlv > PresentationSpanCodec::maximum_ppdu_bytes) { + return std::nullopt; + } + + sizes.mode_inner_tlv = *mode_inner; + sizes.mode_outer_tlv = *mode_outer; + sizes.results_content = *results_content; + sizes.results_tlv = *results_tlv; + sizes.user_tlv = *user_tlv; + sizes.normal_content = *normal_content; + sizes.normal_tlv = *normal_tlv; + sizes.outer_content = *outer_content; + sizes.outer_tlv = *outer_tlv; + return sizes; +} + +} // namespace + +bool PresentationCpView::try_context_id_for_abstract_syntax( + const std::span object_identifier_value, + std::uint32_t& context_id) const noexcept { + return find_context_by_abstract_syntax( + context_definition_list, object_identifier_value, context_id); +} + +std::span +PresentationSpanCodec::acse_abstract_syntax_name() noexcept { + return kAcseAbstractSyntax; +} + +std::span +PresentationSpanCodec::mms_abstract_syntax_name() noexcept { + return kMmsAbstractSyntax; +} + +std::span +PresentationSpanCodec::ber_transfer_syntax_name() noexcept { + return kBerTransferSyntax; +} + +std::optional PresentationSpanCodec::fully_encoded_data_size( + const std::uint32_t context_id, + const std::size_t single_asn1_type_bytes) noexcept { + const auto sizes = calculate_fully_encoded_sizes(context_id, single_asn1_type_bytes); + return sizes ? std::optional{sizes->outer_tlv} : std::nullopt; +} + +wire::EncodeResult PresentationSpanCodec::encode_fully_encoded_data_into( + const std::uint32_t context_id, + const std::span single_asn1_type, + const std::span destination) noexcept { + const auto sizes = calculate_fully_encoded_sizes(context_id, single_asn1_type.size()); + if (!sizes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < sizes->outer_tlv) { + return {wire::EncodeStatus::buffer_too_small, 0U, sizes->outer_tlv}; + } + + asn1::BerSpanWriter writer{destination.first(sizes->outer_tlv)}; + if (!writer.write_tlv_header( + asn1::BerClass::application, true, 1, sizes->sequence_tlv) || + !writer.write_tlv_header( + asn1::BerClass::universal, true, 16, sizes->sequence_content) || + !writer.write_tlv_header( + asn1::BerClass::universal, false, 2, sizes->id_value) || + !write_unsigned_integer(writer, context_id) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, + true, + 0, + single_asn1_type.size()) || + !writer.write_bytes(single_asn1_type) || + writer.size() != sizes->outer_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizes->outer_tlv}; + } + return {wire::EncodeStatus::ok, sizes->outer_tlv, sizes->outer_tlv}; +} + +bool PresentationSpanCodec::try_decode_fully_encoded_data_view( + const std::span bytes, + PresentationPdvView& pdv) noexcept { + pdv = {}; + if (bytes.empty() || bytes.size() > maximum_ppdu_bytes) { + return false; + } + asn1::BerTlvView outer; + return asn1::BerSpanReader::try_read_exact(bytes, outer) && + decode_fully_encoded_outer(outer, pdv); +} + +bool PresentationSpanCodec::try_decode_cp_view( + const std::span bytes, + PresentationCpView& cp) noexcept { + cp = {}; + if (bytes.empty() || bytes.size() > maximum_ppdu_bytes) { + return false; + } + + asn1::BerTlvView outer; + if (!asn1::BerSpanReader::try_read_exact(bytes, outer) || + outer.tag_class != asn1::BerClass::universal || + outer.tag_number != 17 || !outer.constructed) { + return false; + } + + bool saw_mode = false; + bool saw_normal = false; + std::size_t offset = 0U; + while (offset < outer.value.size()) { + asn1::BerTlvView item; + if (!asn1::BerSpanReader::try_read_tlv(outer.value, offset, item)) { + cp = {}; + return false; + } + + if (item.tag_class == asn1::BerClass::context_specific && + item.tag_number == 0 && item.constructed) { + if (saw_mode || !decode_mode_selector(item, cp.mode_selector)) { + cp = {}; + return false; + } + saw_mode = true; + } else if (item.tag_class == asn1::BerClass::context_specific && + item.tag_number == 2 && item.constructed) { + if (saw_normal) { + cp = {}; + return false; + } + bool saw_calling = false; + bool saw_called = false; + bool saw_contexts = false; + bool saw_user_data = false; + std::size_t normal_offset = 0U; + while (normal_offset < item.value.size()) { + asn1::BerTlvView normal; + if (!asn1::BerSpanReader::try_read_tlv( + item.value, normal_offset, normal)) { + cp = {}; + return false; + } + + if (normal.tag_class == asn1::BerClass::context_specific && + normal.tag_number == 1 && !normal.constructed) { + if (saw_calling || normal.value.size() > maximum_selector_bytes) { + cp = {}; + return false; + } + cp.calling_selector = normal.value; + saw_calling = true; + } else if (normal.tag_class == asn1::BerClass::context_specific && + normal.tag_number == 2 && !normal.constructed) { + if (saw_called || normal.value.size() > maximum_selector_bytes) { + cp = {}; + return false; + } + cp.called_selector = normal.value; + saw_called = true; + } else if (normal.tag_class == asn1::BerClass::context_specific && + normal.tag_number == 4 && normal.constructed) { + if (saw_contexts || !validate_context_list( + normal.value, cp.context_count)) { + cp = {}; + return false; + } + cp.context_definition_list = normal.value; + saw_contexts = true; + } else if (normal.tag_class == asn1::BerClass::application && + normal.tag_number == 1 && normal.constructed) { + if (saw_user_data || !decode_fully_encoded_outer( + normal, cp.user_data)) { + cp = {}; + return false; + } + saw_user_data = true; + } else { + cp = {}; + return false; + } + } + if (!saw_contexts || !saw_user_data || + !context_id_exists( + cp.context_definition_list, cp.user_data.context_id)) { + cp = {}; + return false; + } + saw_normal = true; + } else { + cp = {}; + return false; + } + } + + if (!saw_mode || !saw_normal) { + cp = {}; + return false; + } + return true; +} + +std::optional PresentationSpanCodec::cpa_accepting_size( + const PresentationCpView& request, + const std::size_t acse_aare_bytes) noexcept { + const auto sizes = calculate_cpa_sizes(request, acse_aare_bytes); + return sizes ? std::optional{sizes->outer_tlv} : std::nullopt; +} + +wire::EncodeResult PresentationSpanCodec::encode_cpa_accepting_into( + const PresentationCpView& request, + const std::span acse_aare, + const std::span destination) noexcept { + const auto sizes = calculate_cpa_sizes(request, acse_aare.size()); + if (!sizes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + if (destination.size() < sizes->outer_tlv) { + return {wire::EncodeStatus::buffer_too_small, 0U, sizes->outer_tlv}; + } + + asn1::BerSpanWriter writer{destination.first(sizes->outer_tlv)}; + if (!writer.write_tlv_header( + asn1::BerClass::universal, true, 17, sizes->outer_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 0, sizes->mode_inner_tlv) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, sizes->mode_value) || + !write_unsigned_integer(writer, request.mode_selector) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 2, sizes->normal_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, true, 5, sizes->results_content)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizes->outer_tlv}; + } + + std::size_t context_offset = 0U; + while (context_offset < request.context_definition_list.size()) { + asn1::BerTlvView definition; + ContextDefinitionView context; + if (!asn1::BerSpanReader::try_read_tlv( + request.context_definition_list, context_offset, definition) || + !parse_context_definition(definition, context)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizes->outer_tlv}; + } + const auto result_tlv = asn1::BerSpanWriter::tlv_size(0, 1U); + const auto syntax_tlv = asn1::BerSpanWriter::tlv_size( + 1, context.first_transfer_syntax.size()); + const auto sequence_content = add_size(result_tlv, syntax_tlv); + if (!sequence_content || + !writer.write_tlv_header( + asn1::BerClass::universal, true, 16, *sequence_content) || + !writer.write_tlv_header( + asn1::BerClass::context_specific, false, 0, 1U) || + !writer.write_byte(0x00U) || + !writer.write_tlv( + asn1::BerClass::context_specific, + false, + 1, + context.first_transfer_syntax)) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizes->outer_tlv}; + } + } + + const auto user_result = encode_fully_encoded_data_into( + request.user_data.context_id, + acse_aare, + destination.first(sizes->outer_tlv).subspan(writer.size(), sizes->user_tlv)); + if (!user_result.success() || user_result.bytes_written != sizes->user_tlv) { + return {user_result.status, 0U, sizes->outer_tlv}; + } + const auto written = writer.size() + user_result.bytes_written; + if (written != sizes->outer_tlv) { + return {wire::EncodeStatus::value_out_of_range, 0U, sizes->outer_tlv}; + } + return {wire::EncodeStatus::ok, written, sizes->outer_tlv}; +} + +wire::EncodeResult PresentationSpanCodec::encode_p_data_into( + const std::span abstract_syntax_payload, + const std::span destination, + const std::uint32_t presentation_context_id, + const bool include_give_tokens_prefix) noexcept { + const auto fully_encoded = fully_encoded_data_size( + presentation_context_id, abstract_syntax_payload.size()); + if (!fully_encoded) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto prefix = include_give_tokens_prefix ? 4U : 2U; + if (*fully_encoded > std::numeric_limits::max() - prefix) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto required = prefix + *fully_encoded; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + std::size_t output_offset = 0U; + if (include_give_tokens_prefix) { + destination[output_offset++] = SessionSpanCodec::data_transfer_code; + destination[output_offset++] = 0x00U; + } + destination[output_offset++] = SessionSpanCodec::data_transfer_code; + destination[output_offset++] = 0x00U; + + const auto result = encode_fully_encoded_data_into( + presentation_context_id, + abstract_syntax_payload, + destination.subspan(output_offset, *fully_encoded)); + if (!result.success() || result.bytes_written != *fully_encoded) { + return {result.status, 0U, required}; + } + return {wire::EncodeStatus::ok, required, required}; +} + +bool PresentationSpanCodec::try_decode_p_data_view( + const std::span bytes, + PresentationPdvView& pdv) noexcept { + pdv = {}; + std::span presentation = bytes; + SessionDataTransferView transfer; + if (!bytes.empty() && bytes.front() == SessionSpanCodec::data_transfer_code) { + if (!SessionSpanCodec::try_decode_data_transfer_view(bytes, transfer)) { + return false; + } + presentation = transfer.presentation_payload; + } + return try_decode_fully_encoded_data_view(presentation, pdv); +} + +} // namespace ar::iec61850::osi diff --git a/src/osi/session_span.cpp b/src/osi/session_span.cpp new file mode 100644 index 000000000..43353322d --- /dev/null +++ b/src/osi/session_span.cpp @@ -0,0 +1,346 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/osi/session_span.hpp" + +#include +#include +#include +#include +#include +#include + +namespace ar::iec61850::osi { +namespace { + +constexpr std::array kParameter05{ + 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U}; +constexpr std::array kParameter14{0x00U, 0x02U}; +constexpr std::array kParameter33{0x00U, 0x01U}; +constexpr std::array kParameter34{0x00U, 0x01U}; + +const std::array kDefaultParameters{ + SessionParameterView{0x05U, kParameter05}, + SessionParameterView{0x14U, kParameter14}, + SessionParameterView{0x33U, kParameter33}, + SessionParameterView{0x34U, kParameter34}, +}; + +[[nodiscard]] bool encoded_body_size( + const std::span parameters, + const std::span user_data, + std::size_t& body_length) noexcept { + body_length = 0U; + if (parameters.size() > SessionSpanCodec::maximum_parameter_count || + user_data.size() > std::numeric_limits::max()) { + return false; + } + + if (!user_data.empty()) { + const auto encoded_user = user_data.size() + 2U; + if (encoded_user > std::numeric_limits::max()) { + return false; + } + body_length = encoded_user; + } + for (const auto& parameter : parameters) { + if (parameter.code == SessionSpanCodec::user_data_parameter || + parameter.value.size() > std::numeric_limits::max()) { + body_length = 0U; + return false; + } + const auto encoded = parameter.value.size() + 2U; + if (body_length > std::numeric_limits::max() || + encoded > std::numeric_limits::max() - body_length) { + body_length = 0U; + return false; + } + body_length += encoded; + } + return body_length <= std::numeric_limits::max(); +} + +[[nodiscard]] bool write_parameter( + const std::span destination, + std::size_t& offset, + const std::uint8_t code, + const std::span value) noexcept { + if (offset > destination.size() || + value.size() > std::numeric_limits::max() || + value.size() + 2U > destination.size() - offset) { + return false; + } + destination[offset++] = code; + destination[offset++] = static_cast(value.size()); + std::copy( + value.begin(), + value.end(), + destination.begin() + static_cast(offset)); + offset += value.size(); + return true; +} + +} // namespace + +bool SessionSpduView::try_parameter( + const std::uint8_t parameter_code, + std::span& value) const noexcept { + value = {}; + std::size_t offset = 0U; + while (offset < parameter_bytes.size()) { + if (parameter_bytes.size() - offset < 2U) { + return false; + } + const auto current_code = parameter_bytes[offset]; + const auto length = static_cast(parameter_bytes[offset + 1U]); + offset += 2U; + if (length > parameter_bytes.size() - offset) { + return false; + } + if (current_code == parameter_code) { + value = parameter_bytes.subspan(offset, length); + return true; + } + offset += length; + } + return false; +} + +SessionWireKind SessionSpanCodec::kind_from_code(const std::uint8_t code) noexcept { + switch (code) { + case connect_code: + return SessionWireKind::connect; + case accept_code: + return SessionWireKind::accept; + case reject_code: + return SessionWireKind::reject; + case refuse_code: + return SessionWireKind::refuse; + case abort_code: + return SessionWireKind::abort; + case data_transfer_code: + return SessionWireKind::data_transfer; + default: + return SessionWireKind::unknown; + } +} + +std::span SessionSpanCodec::default_parameters() noexcept { + return kDefaultParameters; +} + +bool SessionSpanCodec::try_decode_prefix_view( + const std::span bytes, + SessionSpduView& spdu, + std::size_t& consumed) noexcept { + spdu = {}; + consumed = 0U; + if (bytes.size() < 2U) { + return false; + } + + const auto kind = kind_from_code(bytes[0]); + if (kind == SessionWireKind::unknown) { + return false; + } + + const auto body_length = static_cast(bytes[1]); + const auto total_length = body_length + 2U; + if (total_length > maximum_spdu_bytes || total_length > bytes.size()) { + return false; + } + + std::size_t offset = 2U; + std::size_t parameter_end = total_length; + std::size_t parameter_count = 0U; + bool saw_user_data = false; + std::span user_data; + + while (offset < total_length) { + if (parameter_count >= maximum_parameter_count || + total_length - offset < 2U) { + return false; + } + ++parameter_count; + const auto parameter_start = offset; + const auto code = bytes[offset++]; + const auto length = static_cast(bytes[offset++]); + if (length > total_length - offset) { + return false; + } + + if (code == user_data_parameter) { + if (saw_user_data || offset + length != total_length) { + return false; + } + saw_user_data = true; + parameter_end = parameter_start; + user_data = bytes.subspan(offset, length); + } + offset += length; + } + + spdu.kind = kind; + spdu.code = bytes[0]; + spdu.length_indicator = bytes[1]; + spdu.parameter_bytes = bytes.subspan(2U, parameter_end - 2U); + spdu.user_data = user_data; + consumed = total_length; + return true; +} + +bool SessionSpanCodec::try_decode_view( + const std::span bytes, + SessionSpduView& spdu) noexcept { + std::size_t consumed = 0U; + return try_decode_prefix_view(bytes, spdu, consumed) && consumed == bytes.size(); +} + +wire::EncodeResult SessionSpanCodec::encode_into( + const std::uint8_t code, + const std::span parameters, + const std::span user_data, + const std::span destination) noexcept { + if (kind_from_code(code) == SessionWireKind::unknown) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + std::size_t body_length{}; + if (!encoded_body_size(parameters, user_data, body_length)) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto required = body_length + 2U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + destination[0] = code; + destination[1] = static_cast(body_length); + std::size_t offset = 2U; + for (const auto& parameter : parameters) { + if (!write_parameter(destination, offset, parameter.code, parameter.value)) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + } + if (!user_data.empty() && + !write_parameter(destination, offset, user_data_parameter, user_data)) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (offset != required) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + return {wire::EncodeStatus::ok, required, required}; +} + +wire::EncodeResult SessionSpanCodec::encode_connect_into( + const std::span presentation_payload, + const std::span destination, + const std::span parameters) noexcept { + const auto effective = parameters.empty() ? default_parameters() : parameters; + return encode_into(connect_code, effective, presentation_payload, destination); +} + +wire::EncodeResult SessionSpanCodec::encode_accept_into( + const std::span presentation_payload, + const std::span destination, + const std::span parameters) noexcept { + const auto effective = parameters.empty() ? default_parameters() : parameters; + return encode_into(accept_code, effective, presentation_payload, destination); +} + +wire::EncodeResult SessionSpanCodec::encode_accept_mirroring_into( + const SessionSpduView& connect, + const std::span presentation_payload, + const std::span destination) noexcept { + if (connect.kind != SessionWireKind::connect || + presentation_payload.size() > std::numeric_limits::max()) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto user_bytes = presentation_payload.empty() + ? 0U + : presentation_payload.size() + 2U; + if (user_bytes > std::numeric_limits::max() || + connect.parameter_bytes.size() > + std::numeric_limits::max() - user_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto body_length = connect.parameter_bytes.size() + user_bytes; + const auto required = body_length + 2U; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + destination[0] = accept_code; + destination[1] = static_cast(body_length); + std::copy( + connect.parameter_bytes.begin(), + connect.parameter_bytes.end(), + destination.begin() + 2); + std::size_t offset = 2U + connect.parameter_bytes.size(); + if (!presentation_payload.empty() && + !write_parameter( + destination, + offset, + user_data_parameter, + presentation_payload)) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + return offset == required + ? wire::EncodeResult{wire::EncodeStatus::ok, required, required} + : wire::EncodeResult{wire::EncodeStatus::value_out_of_range, 0U, required}; +} + +wire::EncodeResult SessionSpanCodec::encode_data_transfer_into( + const std::span presentation_payload, + const std::span destination, + const bool include_give_tokens_prefix) noexcept { + const auto prefix = include_give_tokens_prefix ? 4U : 2U; + if (presentation_payload.size() > + std::numeric_limits::max() - prefix) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto required = presentation_payload.size() + prefix; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + std::size_t offset = 0U; + if (include_give_tokens_prefix) { + destination[offset++] = data_transfer_code; + destination[offset++] = 0x00U; + } + destination[offset++] = data_transfer_code; + destination[offset++] = 0x00U; + std::copy( + presentation_payload.begin(), + presentation_payload.end(), + destination.begin() + static_cast(offset)); + return {wire::EncodeStatus::ok, required, required}; +} + +bool SessionSpanCodec::try_decode_data_transfer_view( + const std::span bytes, + SessionDataTransferView& transfer) noexcept { + transfer = {}; + if (bytes.size() < 2U) { + return false; + } + + std::size_t offset = 0U; + if (bytes.size() >= 4U && + bytes[0] == data_transfer_code && bytes[1] == 0x00U && + bytes[2] == data_transfer_code && bytes[3] == 0x00U) { + transfer.has_give_tokens_prefix = true; + offset = 4U; + } else if (bytes[0] == data_transfer_code && bytes[1] == 0x00U) { + transfer.has_give_tokens_prefix = false; + offset = 2U; + } else { + return false; + } + + transfer.presentation_payload = bytes.subspan(offset); + return true; +} + +} // namespace ar::iec61850::osi diff --git a/src/osi/tpkt_span.cpp b/src/osi/tpkt_span.cpp new file mode 100644 index 000000000..aefec4b99 --- /dev/null +++ b/src/osi/tpkt_span.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include + +namespace ar::iec61850::osi { +namespace { + +[[nodiscard]] std::uint16_t read_be_u16( + const std::span bytes, + const std::size_t offset) noexcept { + return static_cast( + (static_cast(bytes[offset]) << 8U) | + static_cast(bytes[offset + 1U])); +} + +void write_be_u16( + const std::span bytes, + const std::size_t offset, + const std::uint16_t value) noexcept { + bytes[offset] = static_cast((value >> 8U) & 0xFFU); + bytes[offset + 1U] = static_cast(value & 0xFFU); +} + +} // namespace + +wire::EncodeResult TpktSpanCodec::encode_into( + const std::span payload, + const std::span destination) noexcept { + if (payload.size() > maximum_payload_bytes) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + + const auto required = payload.size() + header_length; + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + + destination[0] = supported_version; + destination[1] = 0x00U; + write_be_u16(destination, 2U, static_cast(required)); + std::copy( + payload.begin(), + payload.end(), + destination.begin() + static_cast(header_length)); + return {wire::EncodeStatus::ok, required, required}; +} + +TpktPeekResult TpktSpanCodec::peek_frame( + const std::span bytes) noexcept { + if (bytes.size() < header_length) { + return {TpktPeekStatus::need_more, 0U}; + } + if (bytes[0] != supported_version || bytes[1] != 0x00U) { + return {TpktPeekStatus::invalid, 0U}; + } + + const auto declared = static_cast(read_be_u16(bytes, 2U)); + if (declared < header_length) { + return {TpktPeekStatus::invalid, 0U}; + } + if (bytes.size() < declared) { + return {TpktPeekStatus::need_more, declared}; + } + return {TpktPeekStatus::ready, declared}; +} + +bool TpktSpanCodec::try_decode_view( + const std::span bytes, + TpktFrameView& frame) noexcept { + frame = {}; + const auto peek = peek_frame(bytes); + if (!peek.ready() || peek.frame_bytes != bytes.size()) { + return false; + } + + frame.version = bytes[0]; + frame.declared_length = static_cast(peek.frame_bytes); + frame.payload = bytes.subspan(header_length); + return true; +} + +} // namespace ar::iec61850::osi diff --git a/tests/test_mms_server_urcb_report.cpp b/tests/test_mms_server_urcb_report.cpp new file mode 100644 index 000000000..e8941198a --- /dev/null +++ b/tests/test_mms_server_urcb_report.cpp @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/asn1/ber_span_reader.hpp" +#include "ariec61850/mms/information_report_span.hpp" +#include "ariec61850/mms/static_report_connection.hpp" +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_object_table.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/presentation_span.hpp" +#include "ariec61850/osi/session_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kBooleanType{0x83U, 0x00U}; +constexpr std::array kReportTime{ + 0x00U, 0x00U, 0x12U, 0x34U, 0x00U, 0x01U}; + +constexpr std::array kAssociationRequest{ + 0x0DU, 0xB6U, 0x05U, 0x06U, 0x13U, 0x01U, 0x00U, 0x16U, 0x01U, 0x02U, 0x14U, 0x02U, + 0x00U, 0x02U, 0x33U, 0x02U, 0x00U, 0x01U, 0x34U, 0x02U, 0x00U, 0x01U, 0xC1U, 0xA0U, + 0x31U, 0x81U, 0x9DU, 0xA0U, 0x03U, 0x80U, 0x01U, 0x01U, 0xA2U, 0x81U, 0x95U, 0x81U, + 0x04U, 0x00U, 0x00U, 0x00U, 0x01U, 0x82U, 0x04U, 0x00U, 0x00U, 0x00U, 0x01U, 0xA4U, + 0x23U, 0x30U, 0x0FU, 0x02U, 0x01U, 0x01U, 0x06U, 0x04U, 0x52U, 0x01U, 0x00U, 0x01U, + 0x30U, 0x04U, 0x06U, 0x02U, 0x51U, 0x01U, 0x30U, 0x10U, 0x02U, 0x01U, 0x03U, 0x06U, + 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, 0x01U, 0x30U, 0x04U, 0x06U, 0x02U, 0x51U, 0x01U, + 0x61U, 0x62U, 0x30U, 0x60U, 0x02U, 0x01U, 0x01U, 0xA0U, 0x5BU, 0x60U, 0x59U, 0xA1U, + 0x07U, 0x06U, 0x05U, 0x28U, 0xCAU, 0x22U, 0x02U, 0x03U, 0xA2U, 0x07U, 0x06U, 0x05U, + 0x29U, 0x01U, 0x87U, 0x67U, 0x01U, 0xA3U, 0x03U, 0x02U, 0x01U, 0x0CU, 0xA6U, 0x06U, + 0x06U, 0x04U, 0x29U, 0x01U, 0x87U, 0x67U, 0xA7U, 0x03U, 0x02U, 0x01U, 0x0CU, 0xBEU, + 0x33U, 0x28U, 0x31U, 0x06U, 0x02U, 0x51U, 0x01U, 0x02U, 0x01U, 0x03U, 0xA0U, 0x28U, + 0xA8U, 0x26U, 0x80U, 0x03U, 0x00U, 0xFDU, 0xE8U, 0x81U, 0x01U, 0x0AU, 0x82U, 0x01U, + 0x0AU, 0x83U, 0x01U, 0x05U, 0xA4U, 0x16U, 0x80U, 0x01U, 0x01U, 0x81U, 0x03U, 0x05U, + 0xF1U, 0x00U, 0x82U, 0x0CU, 0x03U, 0xEEU, 0x1CU, 0x00U, 0x00U, 0x04U, 0x08U, 0x00U, + 0x00U, 0x79U, 0xEFU, 0x18U}; + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +template +[[nodiscard]] wire::EncodeResult build_data_tpkt( + const std::span session_payload, + std::array& output, + std::array& scratch) noexcept { + const auto cotp = osi::CotpSpanCodec::encode_data_into(session_payload, scratch); + if (!cotp.success()) { + return cotp; + } + return osi::TpktSpanCodec::encode_into( + std::span{scratch}.first(cotp.bytes_written), output); +} + +[[nodiscard]] bool establish( + mms::MmsStaticConnectionRuntime& connection, + const std::span request, + const std::span response, + const std::span workspace, + const std::span scratch) noexcept { + constexpr std::array tpdu_size{0x0AU}; + constexpr std::array source_tsap{0x00U, 0x01U}; + constexpr std::array destination_tsap{0x00U, 0x01U}; + const std::array parameters{ + osi::CotpParameterView{osi::CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + osi::CotpParameterView{osi::CotpSpanCodec::source_tsap_parameter, source_tsap}, + osi::CotpParameterView{osi::CotpSpanCodec::destination_tsap_parameter, destination_tsap}}; + + const auto cr = osi::CotpSpanCodec::encode_connection_request_into( + 0x0001U, parameters, scratch); + if (!cr.success()) { + return false; + } + const auto cr_tpkt = osi::TpktSpanCodec::encode_into( + std::span{scratch}.first(cr.bytes_written), request); + if (!cr_tpkt.success()) { + return false; + } + auto result = connection.process_tcp_window( + std::span{request.data(), cr_tpkt.bytes_written}, + response, + workspace); + if (!result.response_ready() || + connection.state() != mms::MmsStaticConnectionState::awaiting_association) { + return false; + } + + const auto cotp = osi::CotpSpanCodec::encode_data_into(kAssociationRequest, scratch); + if (!cotp.success()) { + return false; + } + const auto association = osi::TpktSpanCodec::encode_into( + std::span{scratch}.first(cotp.bytes_written), request); + if (!association.success()) { + return false; + } + result = connection.process_tcp_window( + std::span{request.data(), association.bytes_written}, + response, + workspace); + return result.response_ready() && + connection.state() == mms::MmsStaticConnectionState::established && + connection.mms_presentation_context_id() == 3U; +} + +[[nodiscard]] bool decode_report_frame( + const std::span frame, + mms::MmsInformationReportView& report) noexcept { + osi::TpktFrameView tpkt; + osi::CotpTpduView cotp; + osi::SessionDataTransferView session; + osi::PresentationPdvView pdv; + return osi::TpktSpanCodec::try_decode_view(frame, tpkt) && + osi::CotpSpanCodec::try_decode_view(tpkt.payload, cotp) && + cotp.kind == osi::CotpWireKind::data && cotp.end_of_transmission && + osi::SessionSpanCodec::try_decode_data_transfer_view(cotp.user_data, session) && + osi::PresentationSpanCodec::try_decode_fully_encoded_data_view( + session.presentation_payload, pdv) && + pdv.context_id == 3U && + mms::MmsInformationReportSpanCodec::try_decode_information_report( + pdv.single_asn1_type, report); +} + +[[nodiscard]] bool decode_unsigned_item( + const mms::MmsReadAccessResultView& item, + std::uint32_t& value) noexcept { + value = 0U; + if (!item.success) { + return false; + } + asn1::BerTlvView tlv; + if (!asn1::BerSpanReader::try_read_exact(item.encoded_data, tlv) || + tlv.tag_class != asn1::BerClass::context_specific || + tlv.tag_number != 6 || tlv.constructed) { + return false; + } + const auto decoded = asn1::BerSpanReader::read_uint32(tlv); + if (!decoded) { + return false; + } + value = *decoded; + return true; +} + +} // namespace + +int main() { + bool relay_state = true; + bool alarm_state = false; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "R1", kBooleanType, read_boolean, &relay_state, false}, + mms::MmsStaticObjectEntry{ + "LD0", "A1", kBooleanType, read_boolean, &alarm_state, false}}; + const mms::MmsStaticObjectTable object_table{objects}; + const std::array members{ + mms::MmsStaticDataSetMember{"LD0", "R1"}, + mms::MmsStaticDataSetMember{"LD0", "A1"}}; + const std::array data_set_entries{ + mms::MmsStaticDataSetEntry{"LD0", "LLN0$Events", members, false}}; + const mms::MmsStaticDataSetTable data_sets{data_set_entries}; + const mms::MmsStaticApplicationDispatcher dispatcher{object_table, data_sets}; + mms::MmsStaticConnectionRuntime connection{dispatcher}; + + const std::array definitions{ + mms::MmsStaticUrcbDefinition{ + "LD0", + "LLN0$RP$Events", + "LD0/LLN0$RP$Events", + "LD0", + "LLN0$Events", + 7U, + {0x7CU, 0x80U}, + 0U, + 0x08U, + 1'000U}}; + std::array states{}; + mms::MmsStaticUrcbRuntime reports{ + definitions, states, object_table, data_sets}; + if (!reports.initialize()) { + return 1; + } + + std::array request{}; + std::array response{}; + std::array workspace{}; + std::array scratch{}; + + auto poll = mms::MmsStaticReportConnection::poll( + connection, reports, 100U, kReportTime, response, workspace); + if (poll.status != mms::MmsStaticReportConnectionStatus::not_established) { + return 2; + } + if (!establish(connection, request, response, workspace, scratch)) { + return 3; + } + if (mms::MmsStaticReportConnection::poll( + connection, reports, 100U, kReportTime, response, workspace).status != + mms::MmsStaticReportConnectionStatus::no_report_due) { + return 4; + } + + if (reports.set_enabled(0U, true, 100U) != mms::MmsStaticUrcbStatus::ok || + reports.request_general_interrogation(0U) != mms::MmsStaticUrcbStatus::ok) { + return 5; + } + const auto* state = reports.state(0U); + if (state == nullptr || state->sequence_number != 0U || + !state->general_interrogation_pending) { + return 6; + } + + std::array tiny_response{}; + poll = mms::MmsStaticReportConnection::poll( + connection, reports, 100U, kReportTime, tiny_response, workspace); + state = reports.state(0U); + if (poll.status != mms::MmsStaticReportConnectionStatus::response_buffer_too_small || + poll.required_response_bytes <= tiny_response.size() || + poll.reason != mms::MmsStaticUrcbReportReason::general_interrogation || + poll.sequence_number != 1U || state == nullptr || state->sequence_number != 0U || + !state->general_interrogation_pending) { + return 7; + } + + poll = mms::MmsStaticReportConnection::poll( + connection, reports, 100U, kReportTime, response, workspace); + state = reports.state(0U); + if (!poll.response_ready() || poll.bytes_written == 0U || + poll.control_block_index != 0U || poll.sequence_number != 1U || + poll.reason != mms::MmsStaticUrcbReportReason::general_interrogation || + state == nullptr || state->sequence_number != 1U || + state->general_interrogation_pending) { + return 8; + } + + mms::MmsInformationReportView report; + if (!decode_report_frame( + std::span{response}.first(poll.bytes_written), report) || + report.item_count != 13U) { + return 9; + } + mms::MmsReadAccessResultView item; + std::uint32_t sequence = 0U; + if (!report.try_item(2U, item) || !decode_unsigned_item(item, sequence) || sequence != 1U) { + return 10; + } + + poll = mms::MmsStaticReportConnection::poll( + connection, reports, 100U, kReportTime, response, workspace); + if (poll.status != mms::MmsStaticReportConnectionStatus::no_report_due) { + return 11; + } + + // Integrity is due at t=1100. A workspace failure must preserve SqNum and due state. + std::array tiny_workspace{}; + poll = mms::MmsStaticReportConnection::poll( + connection, reports, 1'100U, kReportTime, response, tiny_workspace); + state = reports.state(0U); + if (poll.status != mms::MmsStaticReportConnectionStatus::workspace_too_small || + poll.required_workspace_bytes <= tiny_workspace.size() || + poll.reason != mms::MmsStaticUrcbReportReason::integrity || + poll.sequence_number != 2U || state == nullptr || state->sequence_number != 1U || + state->next_integrity_due_ms != 1'100U) { + return 12; + } + + poll = mms::MmsStaticReportConnection::poll( + connection, reports, 1'100U, kReportTime, response, workspace); + state = reports.state(0U); + if (!poll.response_ready() || poll.sequence_number != 2U || + poll.reason != mms::MmsStaticUrcbReportReason::integrity || + state == nullptr || state->sequence_number != 2U || + state->next_integrity_due_ms != 2'100U || + !decode_report_frame( + std::span{response}.first(poll.bytes_written), report) || + !report.try_item(2U, item) || !decode_unsigned_item(item, sequence) || sequence != 2U) { + return 13; + } + + connection.reset(); + poll = mms::MmsStaticReportConnection::poll( + connection, reports, 2'100U, kReportTime, response, workspace); + state = reports.state(0U); + if (poll.status != mms::MmsStaticReportConnectionStatus::not_established || + state == nullptr || state->sequence_number != 2U || + state->next_integrity_due_ms != 2'100U) { + return 14; + } + + return 0; +} diff --git a/tests/test_mms_static_direct_control.cpp b/tests/test_mms_static_direct_control.cpp new file mode 100644 index 000000000..b59f8b07d --- /dev/null +++ b/tests/test_mms_static_direct_control.cpp @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/data_span_codec.hpp" +#include "ariec61850/mms/static_direct_control.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; +using namespace ar::iec61850::mms; + +#define CHECK(condition) do { \ + if (!(condition)) { \ + throw std::runtime_error(std::string{"CHECK failed: "} + #condition + \ + " at " + __FILE__ + ":" + std::to_string(__LINE__)); \ + } \ +} while (false) + +[[nodiscard]] std::vector make_oper( + const bool value, + const std::uint8_t ctl_num, + const bool test, + const std::uint8_t check_bits = 0U, + const std::uint8_t origin_category = 2U) { + constexpr std::array origin_id{'H', 'M', 'I'}; + constexpr std::array no_check{0U}; + const std::array check_byte{check_bits}; + + auto oper = MmsDataValue::structure({ + MmsDataValue::boolean(value), + MmsDataValue::structure({ + MmsDataValue::unsigned_integer(origin_category), + MmsDataValue::octet_string(origin_id), + }), + MmsDataValue::unsigned_integer(ctl_num), + MmsDataValue::utc_time(Iec61850UtcTime{}), + MmsDataValue::boolean(test), + MmsDataValue::bit_string( + 6U, + check_bits == 0U + ? std::span{no_check} + : std::span{check_byte}), + }); + + const auto required = MmsDataSpanCodec::encoded_size(oper); + CHECK(required.has_value()); + std::vector bytes(*required); + const auto encoded = MmsDataSpanCodec::encode_into(oper, bytes); + CHECK(encoded.success()); + CHECK(encoded.bytes_written == bytes.size()); + return bytes; +} + +void valid_oper_updates_live_state() { + MmsStaticDirectBooleanControlState state{}; + MmsStaticDirectBooleanControlBinding binding{&state}; + const auto bytes = make_oper(true, 7U, false); + + MmsStaticDirectBooleanOperate decoded; + CHECK(try_decode_static_direct_boolean_operate(bytes, decoded)); + CHECK(decoded.control_value); + CHECK(decoded.origin_category == 2U); + CHECK(decoded.control_number == 7U); + CHECK(!decoded.test); + CHECK(!decoded.synchro_check); + CHECK(!decoded.interlock_check); + + const auto result = mms_static_direct_boolean_write_oper(&binding, bytes); + CHECK(result.success); + CHECK(state.value == 1U); + CHECK(state.last_control_number == 7U); + CHECK(!state.last_test); + CHECK(state.accepted_operations == 1U); + CHECK(state.rejected_operations == 0U); +} + +void test_oper_is_non_mutating() { + MmsStaticDirectBooleanControlState state{}; + state.value = 1U; + MmsStaticDirectBooleanControlBinding binding{&state}; + const auto bytes = make_oper(false, 8U, true); + + const auto result = mms_static_direct_boolean_write_oper(&binding, bytes); + CHECK(result.success); + CHECK(state.value == 1U); + CHECK(state.last_control_number == 8U); + CHECK(state.last_test); + CHECK(state.accepted_operations == 1U); +} + +void unsupported_check_bits_fail_closed() { + MmsStaticDirectBooleanControlState state{}; + MmsStaticDirectBooleanControlBinding binding{&state}; + const auto bytes = make_oper(true, 9U, false, 0x80U); + + const auto denied = mms_static_direct_boolean_write_oper(&binding, bytes); + CHECK(!denied.success); + CHECK(denied.failure_code == 11U); + CHECK(state.value == 0U); + CHECK(state.rejected_operations == 1U); + + binding.policy.allow_synchro_check = true; + const auto accepted = mms_static_direct_boolean_write_oper(&binding, bytes); + CHECK(accepted.success); + CHECK(state.value == 1U); +} + +void invalid_shape_and_values_are_rejected() { + MmsStaticDirectBooleanControlState state{}; + MmsStaticDirectBooleanControlBinding binding{&state}; + + auto malformed = make_oper(true, 10U, false); + malformed.pop_back(); + auto result = mms_static_direct_boolean_write_oper(&binding, malformed); + CHECK(!result.success); + CHECK(result.failure_code == 7U); + + result = mms_static_direct_boolean_write_oper(&binding, make_oper(true, 0U, false)); + CHECK(!result.success); + CHECK(result.failure_code == 11U); + + result = mms_static_direct_boolean_write_oper(&binding, make_oper(true, 11U, false, 0U, 9U)); + CHECK(!result.success); + CHECK(result.failure_code == 11U); + CHECK(state.value == 0U); +} + +[[nodiscard]] bool reject_backend(void*, bool) noexcept { + return false; +} + +void backend_failure_does_not_publish_state() { + MmsStaticDirectBooleanControlState state{}; + MmsStaticDirectBooleanControlBinding binding{&state, reject_backend, nullptr}; + const auto result = mms_static_direct_boolean_write_oper( + &binding, make_oper(true, 12U, false)); + CHECK(!result.success); + CHECK(result.failure_code == 10U); + CHECK(state.value == 0U); + CHECK(state.accepted_operations == 0U); + CHECK(state.rejected_operations == 1U); +} + +void read_callbacks_match_mms_types() { + MmsStaticDirectBooleanControlState state{}; + state.value = 1U; + std::array bytes{}; + + auto read = mms_static_direct_boolean_read_state(&state, bytes); + CHECK(read.success()); + CHECK(read.bytes_written == 3U); + CHECK(bytes[0] == 0x83U && bytes[1] == 0x01U && bytes[2] == 0xFFU); + + bytes.fill(0U); + read = mms_static_direct_normal_read_ctl_model(nullptr, bytes); + CHECK(read.success()); + CHECK(read.bytes_written == 3U); + CHECK(bytes[0] == 0x86U && bytes[1] == 0x01U && bytes[2] == 0x01U); + + read = mms_static_control_read_unavailable(nullptr, bytes); + CHECK(!read.success()); +} + +} // namespace + +int main() { + try { + valid_oper_updates_live_state(); + test_oper_is_non_mutating(); + unsupported_check_bits_fail_closed(); + invalid_shape_and_values_are_rejected(); + backend_failure_does_not_publish_state(); + read_callbacks_match_mms_types(); + std::cout << "MMS static direct-control tests passed.\n"; + return 0; + } catch (const std::exception& exception) { + std::cerr << exception.what() << '\n'; + return 1; + } +} diff --git a/tests/test_mms_static_server_session.cpp b/tests/test_mms_static_server_session.cpp new file mode 100644 index 000000000..a39a07592 --- /dev/null +++ b/tests/test_mms_static_server_session.cpp @@ -0,0 +1,368 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_server_session.hpp" +#include "ariec61850/mms/static_object_table.hpp" +#include "ariec61850/osi/cotp_span.hpp" +#include "ariec61850/osi/tpkt_span.hpp" + +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +constexpr std::array kAssociationRequest{ + 0x0DU,0xB6U,0x05U,0x06U,0x13U,0x01U,0x00U,0x16U,0x01U,0x02U,0x14U,0x02U, + 0x00U,0x02U,0x33U,0x02U,0x00U,0x01U,0x34U,0x02U,0x00U,0x01U,0xC1U,0xA0U, + 0x31U,0x81U,0x9DU,0xA0U,0x03U,0x80U,0x01U,0x01U,0xA2U,0x81U,0x95U,0x81U, + 0x04U,0x00U,0x00U,0x00U,0x01U,0x82U,0x04U,0x00U,0x00U,0x00U,0x01U,0xA4U, + 0x23U,0x30U,0x0FU,0x02U,0x01U,0x01U,0x06U,0x04U,0x52U,0x01U,0x00U,0x01U, + 0x30U,0x04U,0x06U,0x02U,0x51U,0x01U,0x30U,0x10U,0x02U,0x01U,0x03U,0x06U, + 0x05U,0x28U,0xCAU,0x22U,0x02U,0x01U,0x30U,0x04U,0x06U,0x02U,0x51U,0x01U, + 0x61U,0x62U,0x30U,0x60U,0x02U,0x01U,0x01U,0xA0U,0x5BU,0x60U,0x59U,0xA1U, + 0x07U,0x06U,0x05U,0x28U,0xCAU,0x22U,0x02U,0x03U,0xA2U,0x07U,0x06U,0x05U, + 0x29U,0x01U,0x87U,0x67U,0x01U,0xA3U,0x03U,0x02U,0x01U,0x0CU,0xA6U,0x06U, + 0x06U,0x04U,0x29U,0x01U,0x87U,0x67U,0xA7U,0x03U,0x02U,0x01U,0x0CU,0xBEU, + 0x33U,0x28U,0x31U,0x06U,0x02U,0x51U,0x01U,0x02U,0x01U,0x03U,0xA0U,0x28U, + 0xA8U,0x26U,0x80U,0x03U,0x00U,0xFDU,0xE8U,0x81U,0x01U,0x0AU,0x82U,0x01U, + 0x0AU,0x83U,0x01U,0x05U,0xA4U,0x16U,0x80U,0x01U,0x01U,0x81U,0x03U,0x05U, + 0xF1U,0x00U,0x82U,0x0CU,0x03U,0xEEU,0x1CU,0x00U,0x00U,0x04U,0x08U,0x00U, + 0x00U,0x79U,0xEFU,0x18U}; + +struct FakeTcp final { + std::span inbound; + std::span outbound; + std::size_t inbound_offset{}; + std::size_t outbound_size{}; + std::size_t maximum_receive_chunk{2U}; + std::size_t maximum_send_chunk{3U}; + bool close_when_empty{true}; +}; + +[[nodiscard]] embedded::IoResult fake_receive( + void* context, + const std::span destination) noexcept { + auto& tcp = *static_cast(context); + if (tcp.inbound_offset == tcp.inbound.size()) { + return { + tcp.close_when_empty ? embedded::IoStatus::closed + : embedded::IoStatus::would_block, + 0U}; + } + const auto count = std::min({ + destination.size(), + tcp.maximum_receive_chunk, + tcp.inbound.size() - tcp.inbound_offset}); + std::copy_n( + tcp.inbound.begin() + static_cast(tcp.inbound_offset), + count, + destination.begin()); + tcp.inbound_offset += count; + return {embedded::IoStatus::ok, count}; +} + +[[nodiscard]] embedded::IoResult fake_send( + void* context, + const std::span bytes) noexcept { + auto& tcp = *static_cast(context); + if (tcp.outbound_size == tcp.outbound.size()) { + return {embedded::IoStatus::would_block, 0U}; + } + const auto count = std::min({ + bytes.size(), + tcp.maximum_send_chunk, + tcp.outbound.size() - tcp.outbound_size}); + std::copy_n( + bytes.begin(), + count, + tcp.outbound.begin() + static_cast(tcp.outbound_size)); + tcp.outbound_size += count; + return {embedded::IoStatus::ok, count}; +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] bool establish( + mms::MmsStaticConnectionRuntime& runtime, + std::span request, + std::span response, + std::span workspace, + std::span scratch) noexcept { + constexpr std::array tpdu_size{0x0AU}; + constexpr std::array source_tsap{0x00U, 0x01U}; + constexpr std::array destination_tsap{0x00U, 0x01U}; + const std::array parameters{ + osi::CotpParameterView{osi::CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + osi::CotpParameterView{osi::CotpSpanCodec::source_tsap_parameter, source_tsap}, + osi::CotpParameterView{ + osi::CotpSpanCodec::destination_tsap_parameter, + destination_tsap}}; + + const auto cr = osi::CotpSpanCodec::encode_connection_request_into( + 0x0001U, parameters, scratch); + if (!cr.success()) return false; + const auto cr_tpkt = osi::TpktSpanCodec::encode_into( + scratch.first(cr.bytes_written), request); + if (!cr_tpkt.success()) return false; + auto result = runtime.process_tcp_window( + request.first(cr_tpkt.bytes_written), response, workspace); + if (!result.response_ready() || + runtime.state() != mms::MmsStaticConnectionState::awaiting_association) { + return false; + } + + const auto cotp = osi::CotpSpanCodec::encode_data_into(kAssociationRequest, scratch); + if (!cotp.success()) return false; + const auto association = osi::TpktSpanCodec::encode_into( + scratch.first(cotp.bytes_written), request); + if (!association.success()) return false; + result = runtime.process_tcp_window( + request.first(association.bytes_written), response, workspace); + return result.response_ready() && runtime.association_active() && + runtime.state() == mms::MmsStaticConnectionState::established; +} + +struct LifecycleSink final { + std::size_t calls{}; + std::uint64_t association_id{}; + std::uint64_t now_ms{}; +}; + +[[nodiscard]] std::uint64_t read_now(const void* context) noexcept { + return context == nullptr ? 0U : *static_cast(context); +} + +void association_closed( + void* context, + const std::uint64_t association_id, + const std::uint64_t now_ms) noexcept { + auto* sink = static_cast(context); + if (sink == nullptr) return; + ++sink->calls; + sink->association_id = association_id; + sink->now_ms = now_ms; +} + +[[nodiscard]] bool fragmented_connection_request_is_pumped() noexcept { + constexpr std::array boolean_type{0x83U, 0x00U}; + const bool value = true; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "ESP32S3IOLD0", + "GGIO1$ST$Ind1$stVal", + boolean_type, + read_boolean, + &value}}; + const mms::MmsStaticObjectTable table{objects}; + const mms::MmsStaticApplicationDispatcher dispatcher{table}; + mms::MmsStaticConnectionRuntime runtime{dispatcher}; + + constexpr std::array tpdu_size{0x0AU}; + constexpr std::array source_tsap{0x00U, 0x01U}; + constexpr std::array destination_tsap{0x00U, 0x01U}; + const std::array parameters{ + osi::CotpParameterView{osi::CotpSpanCodec::tpdu_size_parameter, tpdu_size}, + osi::CotpParameterView{osi::CotpSpanCodec::source_tsap_parameter, source_tsap}, + osi::CotpParameterView{ + osi::CotpSpanCodec::destination_tsap_parameter, + destination_tsap}}; + + std::array cotp_bytes{}; + std::array inbound{}; + const auto cotp = osi::CotpSpanCodec::encode_connection_request_into( + 0x0001U, parameters, cotp_bytes); + const auto tpkt = cotp.success() + ? osi::TpktSpanCodec::encode_into( + std::span{cotp_bytes}.first(cotp.bytes_written), + inbound) + : wire::EncodeResult{}; + if (!cotp.success() || !tpkt.success()) { + return false; + } + + std::array outbound{}; + FakeTcp fake{ + std::span{inbound}.first(tpkt.bytes_written), + outbound}; + const embedded::TcpByteStream stream{&fake, fake_send, fake_receive}; + std::array receive{}; + std::array response{}; + std::array workspace{}; + mms::MmsStaticServerSession session{ + runtime, + stream, + {receive, response, workspace}}; + + for (std::size_t iteration = 0U; iteration < 256U; ++iteration) { + const auto result = session.poll_once(); + if (result.terminal()) { + return false; + } + const auto peek = osi::TpktSpanCodec::peek_frame( + std::span{outbound}.first(fake.outbound_size)); + if (peek.status == osi::TpktPeekStatus::ready) { + osi::TpktFrameView response_tpkt; + osi::CotpTpduView response_cotp; + return fake.inbound_offset == tpkt.bytes_written && + session.pending_output_bytes() == 0U && + runtime.state() == + mms::MmsStaticConnectionState::awaiting_association && + osi::TpktSpanCodec::try_decode_view( + std::span{outbound}.first(peek.frame_bytes), + response_tpkt) && + osi::CotpSpanCodec::try_decode_view( + response_tpkt.payload, + response_cotp) && + response_cotp.kind == osi::CotpWireKind::connection_confirm && + response_cotp.destination_reference == 0x0001U; + } + } + return false; +} + +[[nodiscard]] bool oversized_frame_is_bounded() noexcept { + const bool value = false; + constexpr std::array boolean_type{0x83U, 0x00U}; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "X", boolean_type, read_boolean, &value}}; + const mms::MmsStaticObjectTable table{objects}; + const mms::MmsStaticApplicationDispatcher dispatcher{table}; + mms::MmsStaticConnectionRuntime runtime{dispatcher}; + + constexpr std::array declared_large_tpkt{ + 0x03U, 0x00U, 0x04U, 0x00U, 0x02U, 0xF0U, 0x80U, 0x00U}; + std::array discarded_output{}; + FakeTcp fake{declared_large_tpkt, discarded_output}; + fake.maximum_receive_chunk = declared_large_tpkt.size(); + const embedded::TcpByteStream stream{&fake, fake_send, fake_receive}; + std::array receive{}; + std::array response{}; + std::array workspace{}; + mms::MmsStaticServerSession session{ + runtime, + stream, + {receive, response, workspace}}; + + const auto first = session.poll_once(); + const auto second = session.poll_once(); + return first.status == mms::MmsStaticServerSessionStatus::progressed && + second.status == + mms::MmsStaticServerSessionStatus::receive_buffer_full && + second.terminal() && runtime.state() == mms::MmsStaticConnectionState::closed; +} + +[[nodiscard]] bool peer_close_is_reported() noexcept { + const bool value = false; + constexpr std::array boolean_type{0x83U, 0x00U}; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "X", boolean_type, read_boolean, &value}}; + const mms::MmsStaticObjectTable table{objects}; + const mms::MmsStaticApplicationDispatcher dispatcher{table}; + mms::MmsStaticConnectionRuntime runtime{dispatcher}; + std::array empty_inbound{}; + std::array outbound{}; + FakeTcp fake{std::span{empty_inbound}.first(0U), outbound}; + const embedded::TcpByteStream stream{&fake, fake_send, fake_receive}; + std::array receive{}; + std::array response{}; + std::array workspace{}; + mms::MmsStaticServerSession session{ + runtime, + stream, + {receive, response, workspace}}; + const auto result = session.poll_once(); + return result.status == mms::MmsStaticServerSessionStatus::peer_closed && + result.terminal() && runtime.state() == mms::MmsStaticConnectionState::closed; +} + +[[nodiscard]] bool established_peer_close_notifies_once() noexcept { + const bool value = true; + constexpr std::array boolean_type{0x83U, 0x00U}; + const std::array objects{ + mms::MmsStaticObjectEntry{ + "LD0", "X", boolean_type, read_boolean, &value}}; + const mms::MmsStaticObjectTable table{objects}; + const mms::MmsStaticApplicationDispatcher dispatcher{table}; + + std::uint64_t now = 9'000U; + LifecycleSink sink; + mms::MmsStaticConnectionPolicy policy; + policy.association_id = 77U; + policy.owner[0] = 0xA1U; + policy.owner_size = 1U; + policy.now_ms = read_now; + policy.now_context = &now; + policy.association_closed = association_closed; + policy.association_closed_context = &sink; + mms::MmsStaticConnectionRuntime runtime{dispatcher, policy}; + + std::array request{}; + std::array response{}; + std::array workspace{}; + std::array scratch{}; + if (!establish(runtime, request, response, workspace, scratch)) { + return false; + } + + std::array empty_inbound{}; + std::array outbound{}; + FakeTcp fake{std::span{empty_inbound}.first(0U), outbound}; + const embedded::TcpByteStream stream{&fake, fake_send, fake_receive}; + std::array receive{}; + std::array session_response{}; + std::array session_workspace{}; + mms::MmsStaticServerSession session{ + runtime, + stream, + {receive, session_response, session_workspace}}; + + const auto closed = session.poll_once(); + if (closed.status != mms::MmsStaticServerSessionStatus::peer_closed || + !closed.terminal() || runtime.association_active() || + runtime.state() != mms::MmsStaticConnectionState::closed || + sink.calls != 1U || sink.association_id != 77U || sink.now_ms != now) { + return false; + } + + static_cast(session.poll_once()); + runtime.close_transport(); + return sink.calls == 1U; +} + +} // namespace + +int main() { + if (!fragmented_connection_request_is_pumped()) { + return 1; + } + if (!oversized_frame_is_bounded()) { + return 2; + } + if (!peer_close_is_reported()) { + return 3; + } + if (!established_peer_close_notifies_once()) { + return 4; + } + return 0; +} diff --git a/tools/mms_direct_control_server.cpp b/tools/mms_direct_control_server.cpp new file mode 100644 index 000000000..9bfc139af --- /dev/null +++ b/tools/mms_direct_control_server.cpp @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_direct_control.hpp" +#include "ariec61850/mms/static_server_session.hpp" +#include "ariec61850/mms/services.hpp" + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +#ifdef _WIN32 +using SocketHandle = SOCKET; +using SocketLength = int; +constexpr SocketHandle k_invalid_socket = INVALID_SOCKET; +#else +using SocketHandle = int; +using SocketLength = socklen_t; +constexpr SocketHandle k_invalid_socket = -1; +#endif + +struct Options final { + std::string bind_address{"0.0.0.0"}; + std::uint16_t port{8102U}; + std::uint8_t digital_input_mask{}; + std::uint8_t digital_output_mask{}; + bool serve_once{}; +}; + +class SocketSystem final { +public: + SocketSystem() noexcept { +#ifdef _WIN32 + WSADATA data{}; + ready_ = WSAStartup(MAKEWORD(2, 2), &data) == 0; +#else + ready_ = true; +#endif + } + ~SocketSystem() { +#ifdef _WIN32 + if (ready_) WSACleanup(); +#endif + } + SocketSystem(const SocketSystem&) = delete; + SocketSystem& operator=(const SocketSystem&) = delete; + [[nodiscard]] bool ready() const noexcept { return ready_; } +private: + bool ready_{}; +}; + +class OwnedSocket final { +public: + OwnedSocket() noexcept = default; + explicit OwnedSocket(const SocketHandle handle) noexcept : handle_{handle} {} + ~OwnedSocket() { reset(); } + OwnedSocket(const OwnedSocket&) = delete; + OwnedSocket& operator=(const OwnedSocket&) = delete; + OwnedSocket(OwnedSocket&& other) noexcept : handle_{other.release()} {} + OwnedSocket& operator=(OwnedSocket&& other) noexcept { + if (this != &other) reset(other.release()); + return *this; + } + [[nodiscard]] SocketHandle get() const noexcept { return handle_; } + [[nodiscard]] bool valid() const noexcept { return handle_ != k_invalid_socket; } + [[nodiscard]] SocketHandle release() noexcept { + const auto value = handle_; + handle_ = k_invalid_socket; + return value; + } + void reset(const SocketHandle replacement = k_invalid_socket) noexcept { + if (valid()) { +#ifdef _WIN32 + closesocket(handle_); +#else + close(handle_); +#endif + } + handle_ = replacement; + } +private: + SocketHandle handle_{k_invalid_socket}; +}; + +[[nodiscard]] int last_socket_error() noexcept { +#ifdef _WIN32 + return WSAGetLastError(); +#else + return errno; +#endif +} + +[[nodiscard]] bool is_would_block(const int error) noexcept { +#ifdef _WIN32 + return error == WSAEWOULDBLOCK; +#else + return error == EAGAIN || error == EWOULDBLOCK; +#endif +} + +[[nodiscard]] bool is_timeout(const int error) noexcept { +#ifdef _WIN32 + return error == WSAETIMEDOUT; +#else + return error == ETIMEDOUT; +#endif +} + +[[nodiscard]] bool is_interrupted(const int error) noexcept { +#ifdef _WIN32 + return error == WSAEINTR; +#else + return error == EINTR; +#endif +} + +[[nodiscard]] embedded::IoResult socket_receive( + void* context, + const std::span destination) noexcept { + const auto socket = *static_cast(context); +#ifdef _WIN32 + const auto requested = static_cast(std::min( + destination.size(), static_cast(std::numeric_limits::max()))); +#else + const auto requested = destination.size(); +#endif + const auto received = recv( + socket, reinterpret_cast(destination.data()), requested, 0); + if (received > 0) return {embedded::IoStatus::ok, static_cast(received)}; + if (received == 0) return {embedded::IoStatus::closed, 0U}; + const auto error = last_socket_error(); + if (is_would_block(error)) return {embedded::IoStatus::would_block, 0U}; + if (is_timeout(error) || is_interrupted(error)) return {embedded::IoStatus::timeout, 0U}; + return {embedded::IoStatus::io_error, 0U}; +} + +[[nodiscard]] embedded::IoResult socket_send( + void* context, + const std::span bytes) noexcept { + const auto socket = *static_cast(context); +#ifdef _WIN32 + const auto requested = static_cast(std::min( + bytes.size(), static_cast(std::numeric_limits::max()))); +#else + const auto requested = bytes.size(); +#endif +#ifdef MSG_NOSIGNAL + constexpr int flags = MSG_NOSIGNAL; +#else + constexpr int flags = 0; +#endif + const auto sent = send( + socket, reinterpret_cast(bytes.data()), requested, flags); + if (sent > 0) return {embedded::IoStatus::ok, static_cast(sent)}; + if (sent == 0) return {embedded::IoStatus::closed, 0U}; + const auto error = last_socket_error(); + if (is_would_block(error)) return {embedded::IoStatus::would_block, 0U}; + if (is_timeout(error) || is_interrupted(error)) return {embedded::IoStatus::timeout, 0U}; + return {embedded::IoStatus::io_error, 0U}; +} + +template +[[nodiscard]] bool parse_integer(const std::string_view text, Integer& output) noexcept { + auto digits = text; + int base = 10; + if (digits.size() > 2U && digits[0] == '0' && + (digits[1] == 'x' || digits[1] == 'X')) { + digits.remove_prefix(2U); + base = 16; + } + unsigned int parsed{}; + const auto result = std::from_chars( + digits.data(), digits.data() + digits.size(), parsed, base); + if (result.ec != std::errc{} || result.ptr != digits.data() + digits.size() || + parsed > static_cast(std::numeric_limits::max())) { + return false; + } + output = static_cast(parsed); + return true; +} + +void print_help() { + std::cout + << "ARStack61850 bounded Direct-Normal MMS control server\n\n" + << "Usage:\n" + << " ariec61850_mms_direct_control_server [options]\n\n" + << "Options:\n" + << " --bind ADDRESS Bind address (default 0.0.0.0)\n" + << " --port PORT TCP port (default 8102; use 102 for IEC 61850)\n" + << " --di-mask VALUE Initial GGIO1 Ind1..Ind8 bit mask\n" + << " --do-mask VALUE Initial GGIO1 SPCSO1..8 status bit mask\n" + << " --once Serve one TCP connection, then exit\n" + << " --help Show this help\n\n" + << "Control objects: ESP32S3IOLD0/GGIO1.SPCSO1 .. SPCSO8\n" + << "ctlModel: Direct-with-normal-security (1).\n" + << "Check bits are fail-closed; Test=true is acknowledged without changing output.\n"; +} + +[[nodiscard]] bool parse_options( + const int argc, + char** argv, + Options& options, + bool& help) { + help = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument{argv[index]}; + if (argument == "--help" || argument == "-h") { + help = true; + return true; + } + if (argument == "--once") { + options.serve_once = true; + continue; + } + if (index + 1 >= argc) { + std::cerr << "Missing value after " << argument << '\n'; + return false; + } + const std::string_view value{argv[++index]}; + if (argument == "--bind") { + options.bind_address.assign(value); + } else if (argument == "--port") { + if (!parse_integer(value, options.port) || options.port == 0U) return false; + } else if (argument == "--di-mask") { + if (!parse_integer(value, options.digital_input_mask)) return false; + } else if (argument == "--do-mask") { + if (!parse_integer(value, options.digital_output_mask)) return false; + } else { + std::cerr << "Unknown option: " << argument << '\n'; + return false; + } + } + return true; +} + +[[nodiscard]] OwnedSocket create_listener(const Options& options) { + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + const auto service = std::to_string(options.port); + addrinfo* addresses{}; + if (getaddrinfo( + options.bind_address.empty() ? nullptr : options.bind_address.c_str(), + service.c_str(), &hints, &addresses) != 0) { + return {}; + } + + OwnedSocket listener; + for (auto* address = addresses; address != nullptr; address = address->ai_next) { + OwnedSocket candidate{socket( + address->ai_family, address->ai_socktype, address->ai_protocol)}; + if (!candidate.valid()) continue; + constexpr int enabled = 1; + static_cast(setsockopt( + candidate.get(), SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&enabled), + static_cast(sizeof(enabled)))); + const auto address_length = static_cast(address->ai_addrlen); + if (bind(candidate.get(), address->ai_addr, address_length) == 0 && + listen(candidate.get(), 8) == 0) { + listener = std::move(candidate); + break; + } + } + freeaddrinfo(addresses); + return listener; +} + +void set_client_timeouts(const SocketHandle socket) noexcept { +#ifdef _WIN32 + constexpr DWORD timeout_ms = 1000U; + static_cast(setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout_ms), static_cast(sizeof(timeout_ms)))); + static_cast(setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeout_ms), static_cast(sizeof(timeout_ms)))); +#else + constexpr timeval timeout{1, 0}; + static_cast(setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout))); + static_cast(setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout))); +#endif +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) return {wire::EncodeStatus::value_out_of_range, 0U, required}; + if (destination.size() < required) return {wire::EncodeStatus::buffer_too_small, 0U, required}; + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) != 0U ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +struct EncodedValue final { std::span bytes; }; + +[[nodiscard]] wire::EncodeResult read_encoded( + const void* context, + const std::span destination) noexcept { + if (context == nullptr) return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + const auto bytes = static_cast(context)->bytes; + if (destination.size() < bytes.size()) return {wire::EncodeStatus::buffer_too_small, 0U, bytes.size()}; + std::copy(bytes.begin(), bytes.end(), destination.begin()); + return {wire::EncodeStatus::ok, bytes.size(), bytes.size()}; +} + +[[nodiscard]] mms::MmsTypeSpecification scalar( + const mms::MmsTypeKind kind, + std::string name, + const std::optional size = std::nullopt) { + mms::MmsTypeSpecification result; + result.kind = kind; + result.name = std::move(name); + result.size = size; + return result; +} + +[[nodiscard]] mms::MmsTypeSpecification structure( + std::string name, + std::vector children) { + mms::MmsTypeSpecification result; + result.kind = mms::MmsTypeKind::structure; + result.name = std::move(name); + result.children = std::move(children); + return result; +} + +[[nodiscard]] mms::MmsTypeSpecification direct_boolean_oper_spec() { + return structure("Oper", { + scalar(mms::MmsTypeKind::boolean, "ctlVal"), + structure("origin", { + scalar(mms::MmsTypeKind::unsigned_integer, "orCat"), + scalar(mms::MmsTypeKind::octet_string, "orIdent", 64U), + }), + scalar(mms::MmsTypeKind::unsigned_integer, "ctlNum"), + scalar(mms::MmsTypeKind::utc_time, "T"), + scalar(mms::MmsTypeKind::boolean, "Test"), + scalar(mms::MmsTypeKind::bit_string, "Check", 2U), + }); +} + +[[nodiscard]] std::vector build_single_status_ln_type( + const std::string& data_object) { + return mms::MmsServiceCodec::encode_type_specification(structure("", { + structure("ST", {structure(data_object, {scalar(mms::MmsTypeKind::boolean, "stVal")})}) + })); +} + +[[nodiscard]] std::vector build_ggio_type() { + std::vector status; + std::vector config; + std::vector controls; + status.reserve(16U); + config.reserve(8U); + controls.reserve(8U); + for (std::size_t index = 0U; index < 8U; ++index) { + const auto number = std::to_string(index + 1U); + status.push_back(structure("Ind" + number, {scalar(mms::MmsTypeKind::boolean, "stVal")})); + status.push_back(structure("SPCSO" + number, {scalar(mms::MmsTypeKind::boolean, "stVal")})); + config.push_back(structure("SPCSO" + number, { + scalar(mms::MmsTypeKind::unsigned_integer, "ctlModel") + })); + controls.push_back(structure("SPCSO" + number, {direct_boolean_oper_spec()})); + } + return mms::MmsServiceCodec::encode_type_specification(structure("", { + structure("ST", std::move(status)), + structure("CF", std::move(config)), + structure("CO", std::move(controls)), + })); +} + +[[nodiscard]] bool serve_client( + const SocketHandle client, + const mms::MmsStaticApplicationDispatcher& dispatcher) { + mms::MmsStaticConnectionRuntime runtime{dispatcher}; + auto socket_copy = client; + embedded::TcpByteStream stream{&socket_copy, socket_send, socket_receive}; + std::array receive{}; + std::array response{}; + std::array workspace{}; + mms::MmsStaticServerSession session{runtime, stream, {receive, response, workspace}}; + for (;;) { + const auto result = session.poll_once(); + switch (result.status) { + case mms::MmsStaticServerSessionStatus::progressed: + case mms::MmsStaticServerSessionStatus::would_block: + case mms::MmsStaticServerSessionStatus::timed_out: + continue; + case mms::MmsStaticServerSessionStatus::response_pending: + if (result.application_service != mms::MmsWireConfirmedService::unknown && + result.application_status != mms::MmsStaticDispatchStatus::response_ready && + result.application_status != mms::MmsStaticDispatchStatus::object_not_found) { + std::cerr + << "MMS_DIRECT_CONTROL_APPLICATION_DIAGNOSTIC status=" + << static_cast(result.application_status) + << " service=" << static_cast(result.application_service) + << " invokeID=" << result.invoke_id << '\n'; + } + continue; + case mms::MmsStaticServerSessionStatus::application_rejected: + std::cerr << "MMS request rejected; connection remains open.\n"; + continue; + case mms::MmsStaticServerSessionStatus::peer_closed: + return true; + default: + std::cerr + << "MMS_DIRECT_CONTROL_SESSION_TERMINAL sessionStatus=" + << static_cast(result.status) + << " connectionStatus=" + << static_cast(result.connection_status) + << " bytesReceived=" << result.bytes_received + << " bytesSent=" << result.bytes_sent + << " bufferedInput=" << result.buffered_input_bytes + << " pendingOutput=" << result.pending_output_bytes << '\n'; + return false; + } + } +} + +[[nodiscard]] std::uint8_t output_mask( + const std::array& states) noexcept { + std::uint8_t mask{}; + for (std::size_t index = 0U; index < states.size(); ++index) { + if (states[index].value != 0U) mask = static_cast(mask | (1U << index)); + } + return mask; +} + +} // namespace + +int main(int argc, char** argv) { + Options options; + bool help{}; + if (!parse_options(argc, argv, options, help)) return 2; + if (help) { + print_help(); + return 0; + } + + SocketSystem sockets; + if (!sockets.ready()) return 3; + + constexpr std::string_view domain{"ESP32S3IOLD0"}; + constexpr std::array boolean_type{0x83U, 0x00U}; + constexpr std::array unsigned_type{0x86U, 0x00U}; + const auto oper_type = mms::MmsServiceCodec::encode_type_specification(direct_boolean_oper_spec()); + const auto lln0_type = build_single_status_ln_type("Mod"); + const auto lphd1_type = build_single_status_ln_type("PhyHealth"); + const auto ggio1_type = build_ggio_type(); + + constexpr std::array healthy_ln_data{ + 0xA2U, 0x07U, 0xA2U, 0x05U, 0xA2U, 0x03U, 0x83U, 0x01U, 0xFFU}; + const std::array root_values{ + EncodedValue{healthy_ln_data}, EncodedValue{healthy_ln_data}}; + + std::array read_values{}; + read_values[0] = 1U; + read_values[1] = 1U; + for (std::size_t index = 0U; index < 8U; ++index) { + read_values[index + 2U] = static_cast((options.digital_input_mask >> index) & 0x01U); + } + + std::array control_states{}; + std::array control_bindings{}; + std::array input_items{}; + std::array output_status_items{}; + std::array ctl_model_items{}; + std::array oper_items{}; + + for (std::size_t index = 0U; index < 8U; ++index) { + const auto number = std::to_string(index + 1U); + input_items[index] = "GGIO1$ST$Ind" + number + "$stVal"; + output_status_items[index] = "GGIO1$ST$SPCSO" + number + "$stVal"; + ctl_model_items[index] = "GGIO1$CF$SPCSO" + number + "$ctlModel"; + oper_items[index] = "GGIO1$CO$SPCSO" + number + "$Oper"; + control_states[index].value = static_cast( + (options.digital_output_mask >> index) & 0x01U); + control_bindings[index].state = &control_states[index]; + } + + std::array objects{}; + objects[0] = {domain, "LLN0", lln0_type, read_encoded, &root_values[0], false, nullptr, nullptr, nullptr}; + objects[1] = {domain, "LPHD1", lphd1_type, read_encoded, &root_values[1], false, nullptr, nullptr, nullptr}; + objects[2] = {domain, "GGIO1", ggio1_type, mms::mms_static_control_read_unavailable, nullptr, false, nullptr, nullptr, nullptr}; + objects[3] = {domain, "LLN0$ST$Mod$stVal", boolean_type, read_boolean, &read_values[0]}; + objects[4] = {domain, "LPHD1$ST$PhyHealth$stVal", boolean_type, read_boolean, &read_values[1]}; + for (std::size_t index = 0U; index < 8U; ++index) { + objects[5U + index] = {domain, input_items[index], boolean_type, read_boolean, &read_values[index + 2U]}; + const auto base = 13U + index * 3U; + objects[base] = {domain, output_status_items[index], boolean_type, + mms::mms_static_direct_boolean_read_state, &control_states[index]}; + objects[base + 1U] = {domain, ctl_model_items[index], unsigned_type, + mms::mms_static_direct_normal_read_ctl_model, nullptr}; + objects[base + 2U] = {domain, oper_items[index], oper_type, + mms::mms_static_control_read_unavailable, nullptr, false, + mms::mms_static_direct_boolean_write_oper, &control_bindings[index]}; + } + + const mms::MmsStaticObjectTable object_table{objects}; + if (!object_table.valid()) { + std::cerr << "Static MMS control model is invalid.\n"; + return 4; + } + + std::array output_members{}; + for (std::size_t index = 0U; index < output_members.size(); ++index) { + output_members[index] = {domain, output_status_items[index]}; + } + const std::array data_sets{{ + {domain, "LLN0$Outputs", output_members, false} + }}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + if (!data_set_table.valid_against(object_table)) { + std::cerr << "Static output DataSet is invalid.\n"; + return 4; + } + + mms::MmsStaticDispatchPolicy policy; + policy.maximum_write_variables = 1U; + policy.advertise_flattened_child_aliases = true; + const mms::MmsStaticApplicationDispatcher dispatcher{object_table, data_set_table, policy}; + + auto listener = create_listener(options); + if (!listener.valid()) { + std::cerr << "Unable to bind/listen on " << options.bind_address << ':' << options.port << ".\n"; + return 5; + } + + std::cout << "MMS_DIRECT_CONTROL_SERVER_READY bind=" << options.bind_address + << " port=" << options.port + << " domain=" << domain + << " controls=8 ctlModel=1 dataset=ESP32S3IOLD0/LLN0.Outputs" + << " doMask=" << static_cast(output_mask(control_states)) << '\n'; + + do { + sockaddr_storage peer{}; + SocketLength peer_size = static_cast(sizeof(peer)); + OwnedSocket client{accept( + listener.get(), reinterpret_cast(&peer), &peer_size)}; + if (!client.valid()) { + const auto error = last_socket_error(); + if (is_interrupted(error)) continue; + std::cerr << "TCP accept failed with platform error " << error << ".\n"; + return 6; + } + set_client_timeouts(client.get()); + std::cout << "MMS_DIRECT_CONTROL_CLIENT_ACCEPTED\n"; + const auto clean_close = serve_client(client.get(), dispatcher); + std::size_t accepted{}; + std::size_t rejected{}; + for (const auto& state : control_states) { + accepted += state.accepted_operations; + rejected += state.rejected_operations; + } + std::cout << "MMS_DIRECT_CONTROL_STATE doMask=" + << static_cast(output_mask(control_states)) + << " acceptedOps=" << accepted + << " rejectedOps=" << rejected << '\n'; + std::cout << (clean_close ? "MMS_DIRECT_CONTROL_CLIENT_CLOSED" + : "MMS_DIRECT_CONTROL_CLIENT_ABORTED") << '\n'; + } while (!options.serve_once); + + return 0; +} diff --git a/tools/mms_direct_control_server/CMakeLists.txt b/tools/mms_direct_control_server/CMakeLists.txt new file mode 100644 index 000000000..a44feb106 --- /dev/null +++ b/tools/mms_direct_control_server/CMakeLists.txt @@ -0,0 +1,137 @@ +cmake_minimum_required(VERSION 3.20) + +project(ARStack61850MmsR12ServerProfile LANGUAGES CXX) + +get_filename_component(ARSTACK_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) + +set(ARIEC61850_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(ARIEC61850_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(ARIEC61850_BUILD_FUZZERS OFF CACHE BOOL "" FORCE) +set(ARIEC61850_ENABLE_SANITIZERS OFF CACHE BOOL "" FORCE) +set(ARIEC61850_WARNINGS_AS_ERRORS ON CACHE BOOL "" FORCE) + +# Keep the repository's regular targets available, but do not link the public +# MMS server core into this A/B profile. The profile below compiles the current +# codecs with the isolated IED-simulator parity runtime instead. +add_subdirectory("${ARSTACK_ROOT}" arstack-core) + +function(server_strict_warnings target) + if(MSVC) + target_compile_options(${target} PRIVATE /W4 /WX /permissive-) + else() + target_compile_options(${target} PRIVATE + -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Werror) + endif() +endfunction() + +add_library(ariec61850_mms_r12_server_core STATIC + "${ARSTACK_ROOT}/src/asn1/ber_span_reader.cpp" + "${ARSTACK_ROOT}/src/osi/tpkt_span.cpp" + "${ARSTACK_ROOT}/src/osi/cotp_span.cpp" + "${ARSTACK_ROOT}/src/osi/session_span.cpp" + "${ARSTACK_ROOT}/src/osi/presentation_span.cpp" + "${ARSTACK_ROOT}/src/acse/association_span.cpp" + "${ARSTACK_ROOT}/src/acse/association_compat_span.cpp" + "${ARSTACK_ROOT}/src/mms/pdu_span.cpp" + "${ARSTACK_ROOT}/src/mms/pdu_compat_span.cpp" + "${ARSTACK_ROOT}/src/mms/services_span.cpp" + "${ARSTACK_ROOT}/src/mms/write_span.cpp" + "${ARSTACK_ROOT}/src/mms/data_set_span.cpp" + "${ARSTACK_ROOT}/src/mms/information_report_span.cpp" + "${ARSTACK_ROOT}/src/mms/static_object_table.cpp" + "${ARSTACK_ROOT}/src/mms/static_write.cpp" + "${ARSTACK_ROOT}/src/mms/static_data_set_table.cpp" + "${ARSTACK_ROOT}/src/mms/static_dispatcher.cpp" + "${ARSTACK_ROOT}/src/mms/connection_runtime_iedsim.cpp" + "${ARSTACK_ROOT}/src/mms/static_server_session.cpp" + "${ARSTACK_ROOT}/src/mms/static_direct_control.cpp" + "${ARSTACK_ROOT}/src/mms/static_information_report.cpp" + "${ARSTACK_ROOT}/src/mms/static_report_connection.cpp" + "${ARSTACK_ROOT}/src/mms/static_urcb_objects.cpp" + "${ARSTACK_ROOT}/src/mms/static_urcb_runtime.cpp") +target_include_directories(ariec61850_mms_r12_server_core PUBLIC + "${ARSTACK_ROOT}/include") +target_compile_features(ariec61850_mms_r12_server_core PUBLIC cxx_std_20) +server_strict_warnings(ariec61850_mms_r12_server_core) + +add_executable(ariec61850_mms_direct_control_server + "${ARSTACK_ROOT}/tools/mms_direct_control_server.cpp") +target_link_libraries(ariec61850_mms_direct_control_server PRIVATE + ariec61850_mms_r12_server_core + ARIEC61850::core) +if(WIN32) + target_link_libraries(ariec61850_mms_direct_control_server PRIVATE ws2_32) +endif() +target_compile_features(ariec61850_mms_direct_control_server PRIVATE cxx_std_20) +server_strict_warnings(ariec61850_mms_direct_control_server) + +add_executable(ariec61850_mms_urcb_server + "${ARSTACK_ROOT}/tools/mms_urcb_server.cpp") +target_link_libraries(ariec61850_mms_urcb_server PRIVATE + ariec61850_mms_r12_server_core + ARIEC61850::core) +if(WIN32) + target_link_libraries(ariec61850_mms_urcb_server PRIVATE ws2_32) +endif() +target_compile_features(ariec61850_mms_urcb_server PRIVATE cxx_std_20) +server_strict_warnings(ariec61850_mms_urcb_server) + +# Build the repository's operational discovery client inside this isolated +# profile. The loopback CTest below is the integration contract between the +# runnable servers and the client path used by current engineering workflows. +add_executable(ariec61850_mms_live_discover_client + "${ARSTACK_ROOT}/tools/live_discover.cpp") +target_link_libraries(ariec61850_mms_live_discover_client PRIVATE + ARIEC61850::core) +target_compile_features(ariec61850_mms_live_discover_client PRIVATE cxx_std_20) +server_strict_warnings(ariec61850_mms_live_discover_client) + +add_executable(ariec61850_mms_control_inventory_client + "${ARSTACK_ROOT}/tools/control_inventory_probe.cpp") +target_link_libraries(ariec61850_mms_control_inventory_client PRIVATE + ARIEC61850::core) +target_compile_features(ariec61850_mms_control_inventory_client PRIVATE cxx_std_20) +server_strict_warnings(ariec61850_mms_control_inventory_client) + +add_executable(ariec61850_mms_control_interop_client + "${ARSTACK_ROOT}/tools/control_interop_probe.cpp") +target_link_libraries(ariec61850_mms_control_interop_client PRIVATE + ARIEC61850::core) +target_compile_features(ariec61850_mms_control_interop_client PRIVATE cxx_std_20) +server_strict_warnings(ariec61850_mms_control_interop_client) + +include(CTest) +enable_testing() + +add_executable(ariec61850_mms_direct_control_tests + "${ARSTACK_ROOT}/tests/test_mms_static_direct_control.cpp") +target_link_libraries(ariec61850_mms_direct_control_tests PRIVATE + ariec61850_mms_r12_server_core + ARIEC61850::core) +target_compile_features(ariec61850_mms_direct_control_tests PRIVATE cxx_std_20) +server_strict_warnings(ariec61850_mms_direct_control_tests) + +add_executable(ariec61850_mms_server_urcb_tests + "${ARSTACK_ROOT}/tests/test_mms_server_urcb_report.cpp") +target_link_libraries(ariec61850_mms_server_urcb_tests PRIVATE + ariec61850_mms_r12_server_core + ARIEC61850::core) +target_compile_features(ariec61850_mms_server_urcb_tests PRIVATE cxx_std_20) +server_strict_warnings(ariec61850_mms_server_urcb_tests) + +add_test(NAME mms_direct_control_unit COMMAND ariec61850_mms_direct_control_tests) +add_test(NAME mms_direct_control_help COMMAND ariec61850_mms_direct_control_server --help) +add_test(NAME mms_server_urcb_report COMMAND ariec61850_mms_server_urcb_tests) +add_test(NAME mms_urcb_server_help COMMAND ariec61850_mms_urcb_server --help) + +find_package(Python3 REQUIRED COMPONENTS Interpreter) +add_test( + NAME mms_server_live_discovery_loopback + COMMAND + ${Python3_EXECUTABLE} + "${ARSTACK_ROOT}/scripts/test-mms-server-live-discovery.py" + --client $ + --control-inventory $ + --control-client $ + --direct-server $ + --urcb-server $) diff --git a/tools/mms_urcb_server.cpp b/tools/mms_urcb_server.cpp new file mode 100644 index 000000000..54889957b --- /dev/null +++ b/tools/mms_urcb_server.cpp @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ariec61850/mms/static_data_set_table.hpp" +#include "ariec61850/mms/static_dispatcher.hpp" +#include "ariec61850/mms/static_report_connection.hpp" +#include "ariec61850/mms/static_server_session.hpp" +#include "ariec61850/mms/static_urcb_objects.hpp" +#include "ariec61850/mms/static_urcb_runtime.hpp" +#include "ariec61850/mms/services.hpp" + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ar::iec61850; + +#ifdef _WIN32 +using SocketHandle = SOCKET; +using SocketLength = int; +constexpr SocketHandle kInvalidSocket = INVALID_SOCKET; +#else +using SocketHandle = int; +using SocketLength = socklen_t; +constexpr SocketHandle kInvalidSocket = -1; +#endif + +struct Options final { + std::string bind_address{"0.0.0.0"}; + std::uint16_t port{8102U}; + std::uint8_t output_mask{}; + bool serve_once{}; +}; + +class SocketSystem final { +public: + SocketSystem() noexcept { +#ifdef _WIN32 + WSADATA data{}; + ready_ = WSAStartup(MAKEWORD(2, 2), &data) == 0; +#else + ready_ = true; +#endif + } + ~SocketSystem() { +#ifdef _WIN32 + if (ready_) WSACleanup(); +#endif + } + SocketSystem(const SocketSystem&) = delete; + SocketSystem& operator=(const SocketSystem&) = delete; + [[nodiscard]] bool ready() const noexcept { return ready_; } +private: + bool ready_{}; +}; + +class OwnedSocket final { +public: + OwnedSocket() noexcept = default; + explicit OwnedSocket(SocketHandle handle) noexcept : handle_{handle} {} + ~OwnedSocket() { reset(); } + OwnedSocket(const OwnedSocket&) = delete; + OwnedSocket& operator=(const OwnedSocket&) = delete; + OwnedSocket(OwnedSocket&& other) noexcept : handle_{other.release()} {} + OwnedSocket& operator=(OwnedSocket&& other) noexcept { + if (this != &other) reset(other.release()); + return *this; + } + [[nodiscard]] SocketHandle get() const noexcept { return handle_; } + [[nodiscard]] bool valid() const noexcept { return handle_ != kInvalidSocket; } + [[nodiscard]] SocketHandle release() noexcept { + const auto value = handle_; + handle_ = kInvalidSocket; + return value; + } + void reset(SocketHandle replacement = kInvalidSocket) noexcept { + if (valid()) { +#ifdef _WIN32 + closesocket(handle_); +#else + close(handle_); +#endif + } + handle_ = replacement; + } +private: + SocketHandle handle_{kInvalidSocket}; +}; + +[[nodiscard]] int last_socket_error() noexcept { +#ifdef _WIN32 + return WSAGetLastError(); +#else + return errno; +#endif +} + +[[nodiscard]] bool is_would_block(int error) noexcept { +#ifdef _WIN32 + return error == WSAEWOULDBLOCK; +#else + return error == EAGAIN || error == EWOULDBLOCK; +#endif +} + +[[nodiscard]] bool is_timeout(int error) noexcept { +#ifdef _WIN32 + return error == WSAETIMEDOUT; +#else + return error == ETIMEDOUT; +#endif +} + +[[nodiscard]] bool is_interrupted(int error) noexcept { +#ifdef _WIN32 + return error == WSAEINTR; +#else + return error == EINTR; +#endif +} + +[[nodiscard]] embedded::IoResult socket_receive( + void* context, + std::span destination) noexcept { + const auto socket = *static_cast(context); +#ifdef _WIN32 + const auto requested = static_cast(std::min( + destination.size(), static_cast(std::numeric_limits::max()))); +#else + const auto requested = destination.size(); +#endif + const auto received = recv( + socket, reinterpret_cast(destination.data()), requested, 0); + if (received > 0) return {embedded::IoStatus::ok, static_cast(received)}; + if (received == 0) return {embedded::IoStatus::closed, 0U}; + const auto error = last_socket_error(); + if (is_would_block(error)) return {embedded::IoStatus::would_block, 0U}; + if (is_timeout(error) || is_interrupted(error)) return {embedded::IoStatus::timeout, 0U}; + return {embedded::IoStatus::io_error, 0U}; +} + +[[nodiscard]] embedded::IoResult socket_send( + void* context, + std::span bytes) noexcept { + const auto socket = *static_cast(context); +#ifdef _WIN32 + const auto requested = static_cast(std::min( + bytes.size(), static_cast(std::numeric_limits::max()))); +#else + const auto requested = bytes.size(); +#endif +#ifdef MSG_NOSIGNAL + constexpr int flags = MSG_NOSIGNAL; +#else + constexpr int flags = 0; +#endif + const auto sent = send(socket, reinterpret_cast(bytes.data()), requested, flags); + if (sent > 0) return {embedded::IoStatus::ok, static_cast(sent)}; + if (sent == 0) return {embedded::IoStatus::closed, 0U}; + const auto error = last_socket_error(); + if (is_would_block(error)) return {embedded::IoStatus::would_block, 0U}; + if (is_timeout(error) || is_interrupted(error)) return {embedded::IoStatus::timeout, 0U}; + return {embedded::IoStatus::io_error, 0U}; +} + +[[nodiscard]] bool send_all( + SocketHandle socket, + std::span bytes) noexcept { + std::size_t offset{}; + while (offset < bytes.size()) { + auto socket_copy = socket; + const auto result = socket_send(&socket_copy, bytes.subspan(offset)); + if (result.status != embedded::IoStatus::ok || result.transferred == 0U) return false; + offset += result.transferred; + } + return true; +} + +template +[[nodiscard]] bool parse_integer(std::string_view text, Integer& output) noexcept { + auto digits = text; + int base = 10; + if (digits.size() > 2U && digits[0] == '0' && (digits[1] == 'x' || digits[1] == 'X')) { + digits.remove_prefix(2U); + base = 16; + } + unsigned int parsed{}; + const auto result = std::from_chars(digits.data(), digits.data() + digits.size(), parsed, base); + if (result.ec != std::errc{} || result.ptr != digits.data() + digits.size() || + parsed > static_cast(std::numeric_limits::max())) { + return false; + } + output = static_cast(parsed); + return true; +} + +void print_help() { + std::cout + << "ARStack61850 bounded static URCB reporting server\n\n" + << "Usage:\n" + << " ariec61850_mms_urcb_server [options]\n\n" + << "Options:\n" + << " --bind ADDRESS Bind address (default 0.0.0.0)\n" + << " --port PORT TCP port (default 8102; use 102 for IEC 61850)\n" + << " --mask VALUE Initial SPCSO1..8 Boolean status mask\n" + << " --once Serve one TCP connection, then exit\n" + << " --help Show this help\n\n" + << "DataSet: ESP32S3IOLD0/LLN0.Outputs\n" + << "URCB: ESP32S3IOLD0/LLN0$RP$Outputs01\n" + << "First live-report gate: reserve/enable the URCB and write GI=true.\n"; +} + +[[nodiscard]] bool parse_options(int argc, char** argv, Options& options, bool& help) { + help = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument{argv[index]}; + if (argument == "--help" || argument == "-h") { + help = true; + return true; + } + if (argument == "--once") { + options.serve_once = true; + continue; + } + if (index + 1 >= argc) return false; + const std::string_view value{argv[++index]}; + if (argument == "--bind") { + options.bind_address.assign(value); + } else if (argument == "--port") { + if (!parse_integer(value, options.port) || options.port == 0U) return false; + } else if (argument == "--mask") { + if (!parse_integer(value, options.output_mask)) return false; + } else { + return false; + } + } + return true; +} + +[[nodiscard]] OwnedSocket create_listener(const Options& options) { + addrinfo hints{}; + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + const auto service = std::to_string(options.port); + addrinfo* addresses{}; + if (getaddrinfo(options.bind_address.c_str(), service.c_str(), &hints, &addresses) != 0) return {}; + OwnedSocket listener; + for (auto* address = addresses; address != nullptr; address = address->ai_next) { + OwnedSocket candidate{socket(address->ai_family, address->ai_socktype, address->ai_protocol)}; + if (!candidate.valid()) continue; + constexpr int enabled = 1; + static_cast(setsockopt(candidate.get(), SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&enabled), static_cast(sizeof(enabled)))); + const auto address_length = static_cast(address->ai_addrlen); + if (bind(candidate.get(), address->ai_addr, address_length) == 0 && + listen(candidate.get(), 8) == 0) { + listener = std::move(candidate); + break; + } + } + freeaddrinfo(addresses); + return listener; +} + +void set_client_timeouts(SocketHandle socket) noexcept { +#ifdef _WIN32 + constexpr DWORD timeout_ms = 1000U; + static_cast(setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout_ms), static_cast(sizeof(timeout_ms)))); + static_cast(setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeout_ms), static_cast(sizeof(timeout_ms)))); +#else + constexpr timeval timeout{1, 0}; + static_cast(setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout))); + static_cast(setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout))); +#endif +} + +[[nodiscard]] std::uint64_t monotonic_ms() noexcept { + using namespace std::chrono; + return static_cast(duration_cast( + steady_clock::now().time_since_epoch()).count()); +} + +[[nodiscard]] std::uint64_t report_now_ms(const void*) noexcept { + return monotonic_ms(); +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) return {wire::EncodeStatus::value_out_of_range, 0U, required}; + if (destination.size() < required) return {wire::EncodeStatus::buffer_too_small, 0U, required}; + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = *static_cast(context) != 0U ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +struct EncodedValue final { std::span bytes; }; + +[[nodiscard]] wire::EncodeResult read_encoded( + const void* context, + std::span destination) noexcept { + if (context == nullptr) return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + const auto bytes = static_cast(context)->bytes; + if (destination.size() < bytes.size()) return {wire::EncodeStatus::buffer_too_small, 0U, bytes.size()}; + std::copy(bytes.begin(), bytes.end(), destination.begin()); + return {wire::EncodeStatus::ok, bytes.size(), bytes.size()}; +} + +[[nodiscard]] mms::MmsTypeSpecification scalar( + const mms::MmsTypeKind kind, + std::string name, + const std::optional size = std::nullopt) { + mms::MmsTypeSpecification result; + result.kind = kind; + result.name = std::move(name); + result.size = size; + return result; +} + +[[nodiscard]] mms::MmsTypeSpecification structure( + std::string name, + std::vector children) { + mms::MmsTypeSpecification result; + result.kind = mms::MmsTypeKind::structure; + result.name = std::move(name); + result.children = std::move(children); + return result; +} + +[[nodiscard]] std::vector build_ggio_type() { + std::vector statuses; + statuses.reserve(8U); + for (std::size_t index = 0U; index < 8U; ++index) { + statuses.push_back(structure("SPCSO" + std::to_string(index + 1U), { + scalar(mms::MmsTypeKind::boolean, "stVal") + })); + } + return mms::MmsServiceCodec::encode_type_specification(structure("", { + structure("ST", std::move(statuses)) + })); +} + +[[nodiscard]] std::vector build_lln0_type() { + return mms::MmsServiceCodec::encode_type_specification(structure("", { + structure("RP", { + structure("Outputs01", { + scalar(mms::MmsTypeKind::visible_string, "RptID", 255U), + scalar(mms::MmsTypeKind::boolean, "RptEna"), + scalar(mms::MmsTypeKind::boolean, "Resv"), + scalar(mms::MmsTypeKind::visible_string, "DatSet", 255U), + scalar(mms::MmsTypeKind::unsigned_integer, "ConfRev", 32U), + scalar(mms::MmsTypeKind::bit_string, "OptFlds", 10U), + scalar(mms::MmsTypeKind::unsigned_integer, "BufTm", 32U), + scalar(mms::MmsTypeKind::bit_string, "TrgOps", 6U), + scalar(mms::MmsTypeKind::unsigned_integer, "IntgPd", 32U), + scalar(mms::MmsTypeKind::boolean, "GI"), + scalar(mms::MmsTypeKind::unsigned_integer, "SqNum", 32U), + }), + }), + })); +} + +[[nodiscard]] wire::EncodeResult read_unavailable( + const void*, + const std::span) noexcept { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; +} + +[[nodiscard]] bool serve_client( + SocketHandle client, + const mms::MmsStaticApplicationDispatcher& dispatcher, + mms::MmsStaticUrcbRuntime& urcbs) { + mms::MmsStaticConnectionRuntime runtime{dispatcher}; + auto socket_copy = client; + embedded::TcpByteStream stream{&socket_copy, socket_send, socket_receive}; + std::array receive{}; + std::array response{}; + std::array workspace{}; + std::array report_frame{}; + std::array report_workspace{}; + mms::MmsStaticServerSession session{runtime, stream, {receive, response, workspace}}; + + for (;;) { + const auto session_result = session.poll_once(); + switch (session_result.status) { + case mms::MmsStaticServerSessionStatus::peer_closed: + return true; + case mms::MmsStaticServerSessionStatus::transport_error: + case mms::MmsStaticServerSessionStatus::protocol_error: + case mms::MmsStaticServerSessionStatus::receive_buffer_full: + case mms::MmsStaticServerSessionStatus::invalid_configuration: + return false; + default: + break; + } + + if (session.pending_output_bytes() != 0U) continue; + + const auto report = mms::MmsStaticReportConnection::poll( + runtime, urcbs, monotonic_ms(), {}, report_frame, report_workspace); + if (report.status == mms::MmsStaticReportConnectionStatus::response_ready) { + if (!send_all(client, std::span{ + report_frame.data(), report.bytes_written})) { + return false; + } + std::cout << "MMS_URCB_REPORT_SENT index=" << report.control_block_index + << " sqNum=" << static_cast(report.sequence_number) + << " reason=" << static_cast(report.reason) + << " bytes=" << report.bytes_written << '\n'; + } else if (report.status == mms::MmsStaticReportConnectionStatus::report_encode_failed || + report.status == mms::MmsStaticReportConnectionStatus::response_buffer_too_small || + report.status == mms::MmsStaticReportConnectionStatus::workspace_too_small) { + std::cerr << "MMS_URCB_REPORT_ERROR status=" << static_cast(report.status) + << " urcbStatus=" << static_cast(report.urcb_status) << '\n'; + } + } +} + +} // namespace + +int main(int argc, char** argv) { + Options options; + bool help{}; + if (!parse_options(argc, argv, options, help)) return 2; + if (help) { + print_help(); + return 0; + } + + SocketSystem sockets; + if (!sockets.ready()) return 3; + + constexpr std::string_view domain{"ESP32S3IOLD0"}; + constexpr std::array boolean_type{0x83U, 0x00U}; + const auto lln0_type = build_lln0_type(); + const auto ggio_type = build_ggio_type(); + const EncodedValue ggio_root{ggio_type}; + + std::array values{}; + std::array status_items{}; + std::array base_objects{}; + base_objects[0] = {domain, "LLN0", lln0_type, read_unavailable, nullptr}; + base_objects[1] = {domain, "GGIO1", ggio_type, read_encoded, &ggio_root}; + for (std::size_t index = 0U; index < 8U; ++index) { + values[index] = static_cast((options.output_mask >> index) & 0x01U); + status_items[index] = "GGIO1$ST$SPCSO" + std::to_string(index + 1U) + "$stVal"; + base_objects[index + 2U] = { + domain, status_items[index], boolean_type, read_boolean, &values[index] + }; + } + const mms::MmsStaticObjectTable base_table{base_objects}; + if (!base_table.valid()) return 4; + + std::array members{}; + for (std::size_t index = 0U; index < members.size(); ++index) { + members[index] = {domain, status_items[index]}; + } + const std::array data_sets{{ + {domain, "LLN0$Outputs", members, false} + }}; + const mms::MmsStaticDataSetTable data_set_table{data_sets}; + if (!data_set_table.valid_against(base_table)) return 4; + + constexpr std::array opt_fields{0x5CU, 0x80U}; + const std::array definitions{{ + {domain, "LLN0$RP$Outputs01", "ARSTACK-Outputs01", + domain, "LLN0$Outputs", 1U, opt_fields, 0U, 0U, 0U} + }}; + std::array states{}; + mms::MmsStaticUrcbRuntime urcbs{definitions, states, base_table, data_set_table}; + if (!urcbs.initialize()) return 4; + + std::array object_storage{}; + std::array context_storage{}; + std::array name_storage{}; + mms::MmsStaticUrcbObjectBank object_bank{ + urcbs, base_objects, object_storage, context_storage, name_storage, + report_now_ms, nullptr}; + if (!object_bank.initialize()) return 4; + + mms::MmsStaticDispatchPolicy policy; + policy.maximum_write_variables = 1U; + policy.advertise_flattened_child_aliases = true; + const mms::MmsStaticApplicationDispatcher dispatcher{ + object_bank.table(), data_set_table, policy}; + + auto listener = create_listener(options); + if (!listener.valid()) { + std::cerr << "Unable to bind/listen on " << options.bind_address << ':' << options.port << ".\n"; + return 5; + } + + std::cout << "MMS_URCB_SERVER_READY bind=" << options.bind_address + << " port=" << options.port + << " dataset=ESP32S3IOLD0/LLN0.Outputs" + << " urcb=ESP32S3IOLD0/LLN0$RP$Outputs01" + << " members=8 mask=" << static_cast(options.output_mask) << '\n'; + + do { + static_cast(urcbs.set_enabled(0U, false, monotonic_ms())); + static_cast(urcbs.set_reserved(0U, false)); + sockaddr_storage peer{}; + SocketLength peer_size = static_cast(sizeof(peer)); + OwnedSocket client{accept(listener.get(), reinterpret_cast(&peer), &peer_size)}; + if (!client.valid()) { + const auto error = last_socket_error(); + if (is_interrupted(error)) continue; + return 6; + } + set_client_timeouts(client.get()); + std::cout << "MMS_URCB_CLIENT_ACCEPTED\n"; + const auto clean_close = serve_client(client.get(), dispatcher, urcbs); + const auto state = urcbs.state(0U); + std::cout << "MMS_URCB_STATE enabled=" << (state != nullptr && state->enabled ? "true" : "false") + << " reserved=" << (state != nullptr && state->reserved ? "true" : "false") + << " sqNum=" << (state != nullptr ? static_cast(state->sequence_number) : 0U) + << '\n'; + static_cast(urcbs.set_enabled(0U, false, monotonic_ms())); + static_cast(urcbs.set_reserved(0U, false)); + std::cout << (clean_close ? "MMS_URCB_CLIENT_CLOSED" : "MMS_URCB_CLIENT_ABORTED") << '\n'; + } while (!options.serve_once); + + return 0; +} diff --git a/tools/static_ied_server.cpp b/tools/static_ied_server.cpp new file mode 100644 index 000000000..2a969af88 --- /dev/null +++ b/tools/static_ied_server.cpp @@ -0,0 +1,709 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "ariec61850/mms/static_server_session.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#include +#include +#include +#include +#include +#endif + +namespace { +namespace embedded = ar::iec61850::embedded; +namespace mms = ar::iec61850::mms; +namespace wire = ar::iec61850::wire; + +std::atomic_bool g_stop{false}; + +void signal_handler(int) { + g_stop.store(true, std::memory_order_relaxed); +} + +struct SocketRuntime final { +#if defined(_WIN32) + SocketRuntime() { + WSADATA data{}; + if (::WSAStartup(MAKEWORD(2, 2), &data) != 0) { + throw std::runtime_error("WSAStartup failed."); + } + } + ~SocketRuntime() { ::WSACleanup(); } +#else + SocketRuntime() = default; +#endif +}; + +#if defined(_WIN32) +using NativeSocket = SOCKET; +constexpr NativeSocket kInvalidSocket = INVALID_SOCKET; +#else +using NativeSocket = int; +constexpr NativeSocket kInvalidSocket = -1; +#endif + +enum class SocketWaitStatus : std::uint8_t { + ready, + timeout, + interrupted, + error, +}; + +void close_socket(const NativeSocket socket) noexcept { + if (socket == kInvalidSocket) { + return; + } +#if defined(_WIN32) + static_cast(::closesocket(socket)); +#else + static_cast(::close(socket)); +#endif +} + +[[nodiscard]] std::string socket_error_text() { +#if defined(_WIN32) + return std::to_string(::WSAGetLastError()); +#else + return std::to_string(errno); +#endif +} + +[[nodiscard]] bool socket_interrupted() noexcept { +#if defined(_WIN32) + return ::WSAGetLastError() == WSAEINTR; +#else + return errno == EINTR; +#endif +} + +[[nodiscard]] SocketWaitStatus wait_socket( + const NativeSocket socket, + const bool for_read, + const std::uint32_t timeout_ms) noexcept { + fd_set read_set; + fd_set write_set; + FD_ZERO(&read_set); + FD_ZERO(&write_set); + if (for_read) { + FD_SET(socket, &read_set); + } else { + FD_SET(socket, &write_set); + } + + timeval timeout{}; + timeout.tv_sec = static_cast(timeout_ms / 1'000U); + timeout.tv_usec = static_cast((timeout_ms % 1'000U) * 1'000U); +#if defined(_WIN32) + const auto result = ::select( + 0, + for_read ? &read_set : nullptr, + for_read ? nullptr : &write_set, + nullptr, + &timeout); +#else + const auto result = ::select( + socket + 1, + for_read ? &read_set : nullptr, + for_read ? nullptr : &write_set, + nullptr, + &timeout); +#endif + if (result > 0) { + return SocketWaitStatus::ready; + } + if (result == 0) { + return SocketWaitStatus::timeout; + } + return socket_interrupted() + ? SocketWaitStatus::interrupted + : SocketWaitStatus::error; +} + +[[nodiscard]] NativeSocket create_listener(const std::uint16_t port) { + const auto listener = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listener == kInvalidSocket) { + throw std::runtime_error("socket() failed: " + socket_error_text()); + } + + int yes = 1; +#if defined(_WIN32) + static_cast(::setsockopt( + listener, + SOL_SOCKET, + SO_REUSEADDR, + reinterpret_cast(&yes), + static_cast(sizeof(yes)))); +#else + static_cast(::setsockopt( + listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))); +#endif + + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_ANY); + address.sin_port = htons(port); +#if defined(_WIN32) + const auto address_size = static_cast(sizeof(address)); +#else + const auto address_size = static_cast(sizeof(address)); +#endif + if (::bind( + listener, + reinterpret_cast(&address), + address_size) != 0) { + const auto error = socket_error_text(); + close_socket(listener); + throw std::runtime_error("bind() failed: " + error); + } + if (::listen(listener, 4) != 0) { + const auto error = socket_error_text(); + close_socket(listener); + throw std::runtime_error("listen() failed: " + error); + } + return listener; +} + +struct SocketStreamContext final { + NativeSocket socket{kInvalidSocket}; +}; + +[[nodiscard]] embedded::IoResult socket_receive( + void* context, + const std::span destination) noexcept { + if (context == nullptr || destination.empty()) { + return {embedded::IoStatus::invalid_argument, 0U}; + } + auto& stream = *static_cast(context); + if (stream.socket == kInvalidSocket) { + return {embedded::IoStatus::closed, 0U}; + } + + const auto readiness = wait_socket(stream.socket, true, 100U); + if (readiness == SocketWaitStatus::timeout) { + return {embedded::IoStatus::timeout, 0U}; + } + if (readiness == SocketWaitStatus::interrupted) { + return {embedded::IoStatus::would_block, 0U}; + } + if (readiness != SocketWaitStatus::ready) { + return {embedded::IoStatus::io_error, 0U}; + } + +#if defined(_WIN32) + const auto bounded = std::min( + destination.size(), static_cast(INT_MAX)); + const auto count = ::recv( + stream.socket, + reinterpret_cast(destination.data()), + static_cast(bounded), + 0); + if (count > 0) { + return {embedded::IoStatus::ok, static_cast(count)}; + } + if (count == 0) { + return {embedded::IoStatus::closed, 0U}; + } + const auto error = ::WSAGetLastError(); + if (error == WSAEWOULDBLOCK || error == WSAEINTR) { + return {embedded::IoStatus::would_block, 0U}; + } + if (error == WSAETIMEDOUT) { + return {embedded::IoStatus::timeout, 0U}; + } +#else + const auto count = ::recv(stream.socket, destination.data(), destination.size(), 0); + if (count > 0) { + return {embedded::IoStatus::ok, static_cast(count)}; + } + if (count == 0) { + return {embedded::IoStatus::closed, 0U}; + } + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { + return {embedded::IoStatus::would_block, 0U}; + } + if (errno == ETIMEDOUT) { + return {embedded::IoStatus::timeout, 0U}; + } +#endif + return {embedded::IoStatus::io_error, 0U}; +} + +[[nodiscard]] embedded::IoResult socket_send( + void* context, + const std::span bytes) noexcept { + if (context == nullptr || bytes.empty()) { + return {embedded::IoStatus::invalid_argument, 0U}; + } + auto& stream = *static_cast(context); + if (stream.socket == kInvalidSocket) { + return {embedded::IoStatus::closed, 0U}; + } + + const auto readiness = wait_socket(stream.socket, false, 100U); + if (readiness == SocketWaitStatus::timeout) { + return {embedded::IoStatus::timeout, 0U}; + } + if (readiness == SocketWaitStatus::interrupted) { + return {embedded::IoStatus::would_block, 0U}; + } + if (readiness != SocketWaitStatus::ready) { + return {embedded::IoStatus::io_error, 0U}; + } + +#if defined(_WIN32) + const auto bounded = std::min( + bytes.size(), static_cast(INT_MAX)); + const auto count = ::send( + stream.socket, + reinterpret_cast(bytes.data()), + static_cast(bounded), + 0); + if (count > 0) { + return {embedded::IoStatus::ok, static_cast(count)}; + } + if (count == 0) { + return {embedded::IoStatus::closed, 0U}; + } + const auto error = ::WSAGetLastError(); + if (error == WSAEWOULDBLOCK || error == WSAEINTR) { + return {embedded::IoStatus::would_block, 0U}; + } + if (error == WSAETIMEDOUT) { + return {embedded::IoStatus::timeout, 0U}; + } +#else + const auto count = ::send(stream.socket, bytes.data(), bytes.size(), 0); + if (count > 0) { + return {embedded::IoStatus::ok, static_cast(count)}; + } + if (count == 0) { + return {embedded::IoStatus::closed, 0U}; + } + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) { + return {embedded::IoStatus::would_block, 0U}; + } + if (errno == ETIMEDOUT) { + return {embedded::IoStatus::timeout, 0U}; + } +#endif + return {embedded::IoStatus::io_error, 0U}; +} + +struct CliOptions final { + std::uint16_t port{102U}; + std::uint8_t digital_input_mask{}; + std::size_t maximum_connections{}; +}; + +[[nodiscard]] std::uint32_t parse_u32( + const std::string& option, + const std::string& text, + const std::uint32_t maximum) { + std::size_t consumed = 0U; + const auto value = std::stoull(text, &consumed, 0); + if (consumed != text.size() || value > maximum) { + throw std::invalid_argument(option + " is outside the supported range."); + } + return static_cast(value); +} + +void print_usage() { + std::cout + << "Usage: ariec61850_static_ied_server [options]\n\n" + << "Options:\n" + << " --port N TCP listen port (default 102).\n" + << " --digital-input-mask N GGIO1 Ind1..Ind8 bit mask (default 0).\n" + << " --max-connections N Exit after N accepted TCP connections (default unlimited).\n" + << " -h, --help Show this help.\n\n" + << "Portable bounded static IEC 61850 MMS server for lab/interoperability work.\n" + << "The tool exposes a fixed static object model; it does not claim IEC 61850 conformance.\n"; +} + +[[nodiscard]] CliOptions parse_cli(const int argc, char** argv) { + CliOptions options; + for (int index = 1; index < argc; ++index) { + const std::string option = argv[index]; + if (option == "-h" || option == "--help") { + print_usage(); + std::exit(0); + } + if (option == "--port" || option == "--digital-input-mask" || + option == "--max-connections") { + if (++index >= argc) { + throw std::invalid_argument(option + " requires a value."); + } + const std::string value = argv[index]; + if (option == "--port") { + const auto parsed = parse_u32(option, value, 65'535U); + if (parsed == 0U) { + throw std::invalid_argument("--port must be 1..65535."); + } + options.port = static_cast(parsed); + } else if (option == "--digital-input-mask") { + options.digital_input_mask = + static_cast(parse_u32(option, value, 0xFFU)); + } else { + options.maximum_connections = static_cast( + parse_u32( + option, + value, + std::numeric_limits::max())); + } + continue; + } + throw std::invalid_argument("Unknown option: " + option); + } + return options; +} + +struct EncodedValue final { + std::span bytes; +}; + +[[nodiscard]] wire::EncodeResult read_encoded( + const void* context, + const std::span destination) noexcept { + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, 0U}; + } + const auto& value = *static_cast(context); + if (destination.size() < value.bytes.size()) { + return { + wire::EncodeStatus::buffer_too_small, + 0U, + value.bytes.size()}; + } + std::copy(value.bytes.begin(), value.bytes.end(), destination.begin()); + return { + wire::EncodeStatus::ok, + value.bytes.size(), + value.bytes.size()}; +} + +[[nodiscard]] wire::EncodeResult read_boolean( + const void* context, + const std::span destination) noexcept { + constexpr std::size_t required = 3U; + if (context == nullptr) { + return {wire::EncodeStatus::value_out_of_range, 0U, required}; + } + if (destination.size() < required) { + return {wire::EncodeStatus::buffer_too_small, 0U, required}; + } + const auto value = *static_cast(context) != 0U; + destination[0] = 0x83U; + destination[1] = 0x01U; + destination[2] = value ? 0xFFU : 0x00U; + return {wire::EncodeStatus::ok, required, required}; +} + +[[nodiscard]] std::vector encode_ber_length( + const std::size_t length) { + if (length < 0x80U) { + return {static_cast(length)}; + } + if (length <= 0xFFU) { + return {0x81U, static_cast(length)}; + } + return { + 0x82U, + static_cast((length >> 8U) & 0xFFU), + static_cast(length & 0xFFU)}; +} + +[[nodiscard]] std::vector make_tlv( + const std::uint8_t tag, + const std::span content) { + std::vector bytes; + const auto length = encode_ber_length(content.size()); + bytes.reserve(1U + length.size() + content.size()); + bytes.push_back(tag); + bytes.insert(bytes.end(), length.begin(), length.end()); + bytes.insert(bytes.end(), content.begin(), content.end()); + return bytes; +} + +[[nodiscard]] std::vector concat( + const std::initializer_list> parts) { + std::size_t total = 0U; + for (const auto part : parts) { + total += part.size(); + } + std::vector result; + result.reserve(total); + for (const auto part : parts) { + result.insert(result.end(), part.begin(), part.end()); + } + return result; +} + +[[nodiscard]] std::vector build_single_status_ln_type( + const std::string_view do_name) { + constexpr std::array boolean_type{0x83U, 0x00U}; + const auto da_name = make_tlv( + 0x1AU, + std::span{ + reinterpret_cast("stVal"), 5U}); + const auto da = make_tlv(0xA2U, concat({da_name, boolean_type})); + const auto da_list = make_tlv(0xA2U, da); + const auto do_name_tlv = make_tlv( + 0x1AU, + std::span{ + reinterpret_cast(do_name.data()), do_name.size()}); + const auto do_entry = make_tlv(0xA2U, concat({do_name_tlv, da_list})); + return make_tlv(0xA2U, do_entry); +} + +[[nodiscard]] std::vector build_ggio_type() { + std::vector do_entries; + for (std::size_t index = 1U; index <= 8U; ++index) { + const auto do_name = std::string{"Ind"} + std::to_string(index); + const auto encoded = build_single_status_ln_type(do_name); + const auto inner = std::span{encoded}.subspan(2U); + do_entries.insert(do_entries.end(), inner.begin(), inner.end()); + } + return make_tlv(0xA2U, do_entries); +} + +struct ConnectionBuffers final { + std::array receive{}; + std::array response{}; + std::array workspace{}; +}; + +void serve_connection( + const NativeSocket socket, + const mms::MmsStaticObjectTable& object_table, + const mms::MmsStaticDataSetTable& data_sets, + const std::uint64_t association_id) { + const mms::MmsStaticApplicationDispatcher dispatcher{object_table, data_sets}; + + mms::MmsStaticConnectionPolicy policy; + policy.association_id = association_id; + policy.owner_size = 8U; + for (std::size_t index = 0U; index < policy.owner_size; ++index) { + const auto shift = static_cast((policy.owner_size - 1U - index) * 8U); + policy.owner[index] = static_cast( + (association_id >> shift) & 0xFFU); + } + + mms::MmsStaticConnectionRuntime runtime{dispatcher, policy}; + SocketStreamContext socket_context{socket}; + const embedded::TcpByteStream stream{ + &socket_context, + socket_send, + socket_receive}; + ConnectionBuffers buffers{}; + mms::MmsStaticServerSession session{ + runtime, + stream, + {buffers.receive, buffers.response, buffers.workspace}}; + + while (!g_stop.load(std::memory_order_relaxed)) { + const auto result = session.poll_once(); + if (result.terminal()) { + return; + } + if (result.status == mms::MmsStaticServerSessionStatus::would_block || + result.status == mms::MmsStaticServerSessionStatus::timed_out) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } + } + runtime.close_transport(); +} + +} // namespace + +int main(int argc, char** argv) { + try { + const auto options = parse_cli(argc, argv); + [[maybe_unused]] SocketRuntime socket_runtime; + std::signal(SIGINT, signal_handler); +#if !defined(_WIN32) + std::signal(SIGTERM, signal_handler); +#endif + + constexpr std::array boolean_type{0x83U, 0x00U}; + std::array values{}; + values[0] = 1U; + values[1] = 1U; + for (std::size_t index = 0U; index < 8U; ++index) { + values[index + 2U] = static_cast( + (options.digital_input_mask >> index) & 0x01U); + } + + const auto lln0_type = build_single_status_ln_type("Mod"); + const auto lphd1_type = build_single_status_ln_type("PhyHealth"); + const auto ggio1_type = build_ggio_type(); + constexpr std::array healthy_ln_data{ + 0xA2U, 0x07U, 0xA2U, 0x05U, 0xA2U, 0x03U, 0x83U, 0x01U, 0xFFU}; + std::array ggio_data{}; + ggio_data[0] = 0xA2U; + ggio_data[1] = 0x2AU; + ggio_data[2] = 0xA2U; + ggio_data[3] = 0x28U; + for (std::size_t index = 0U; index < 8U; ++index) { + const auto offset = 4U + index * 5U; + ggio_data[offset] = 0xA2U; + ggio_data[offset + 1U] = 0x03U; + ggio_data[offset + 2U] = 0x83U; + ggio_data[offset + 3U] = 0x01U; + ggio_data[offset + 4U] = + values[index + 2U] != 0U ? 0xFFU : 0x00U; + } + const std::array root_values{ + EncodedValue{healthy_ln_data}, + EncodedValue{healthy_ln_data}, + EncodedValue{ggio_data}}; + + std::array objects{}; + objects[0] = mms::MmsStaticObjectEntry{ + "ESP32S3IOLD0", + "LLN0", + lln0_type, + read_encoded, + &root_values[0]}; + objects[1] = mms::MmsStaticObjectEntry{ + "ESP32S3IOLD0", + "LPHD1", + lphd1_type, + read_encoded, + &root_values[1]}; + objects[2] = mms::MmsStaticObjectEntry{ + "ESP32S3IOLD0", + "GGIO1", + ggio1_type, + read_encoded, + &root_values[2]}; + objects[3] = mms::MmsStaticObjectEntry{ + "ESP32S3IOLD0", + "LLN0$ST$Mod$stVal", + boolean_type, + read_boolean, + &values[0]}; + objects[4] = mms::MmsStaticObjectEntry{ + "ESP32S3IOLD0", + "LPHD1$ST$PhyHealth$stVal", + boolean_type, + read_boolean, + &values[1]}; + constexpr std::array leaf_items{ + "GGIO1$ST$Ind1$stVal", + "GGIO1$ST$Ind2$stVal", + "GGIO1$ST$Ind3$stVal", + "GGIO1$ST$Ind4$stVal", + "GGIO1$ST$Ind5$stVal", + "GGIO1$ST$Ind6$stVal", + "GGIO1$ST$Ind7$stVal", + "GGIO1$ST$Ind8$stVal"}; + for (std::size_t index = 0U; index < leaf_items.size(); ++index) { + objects[index + 5U] = mms::MmsStaticObjectEntry{ + "ESP32S3IOLD0", + leaf_items[index], + boolean_type, + read_boolean, + &values[index + 2U]}; + } + + constexpr std::array data_set_members{{ + {"ESP32S3IOLD0", "GGIO1$ST$Ind1$stVal"}, + {"ESP32S3IOLD0", "GGIO1$ST$Ind2$stVal"}, + {"ESP32S3IOLD0", "GGIO1$ST$Ind3$stVal"}, + {"ESP32S3IOLD0", "GGIO1$ST$Ind4$stVal"}, + {"ESP32S3IOLD0", "GGIO1$ST$Ind5$stVal"}, + {"ESP32S3IOLD0", "GGIO1$ST$Ind6$stVal"}, + {"ESP32S3IOLD0", "GGIO1$ST$Ind7$stVal"}, + {"ESP32S3IOLD0", "GGIO1$ST$Ind8$stVal"}}}; + const std::array data_set_entries{{ + {"ESP32S3IOLD0", "LLN0$EventData", data_set_members, false}}}; + + const mms::MmsStaticObjectTable object_table{objects}; + const mms::MmsStaticDataSetTable data_sets{data_set_entries}; + if (!object_table.valid() || + !data_sets.valid() || + !data_sets.valid_against(object_table)) { + throw std::runtime_error("Static MMS server model is invalid."); + } + + const auto listener = create_listener(options.port); + std::cout << "STATIC_IED_SERVER_READY port=" << options.port + << " objects=" << objects.size() + << " datasets=" << data_set_entries.size() << '\n'; + std::cout.flush(); + + std::size_t connection_count = 0U; + while (!g_stop.load(std::memory_order_relaxed) && + (options.maximum_connections == 0U || + connection_count < options.maximum_connections)) { + const auto readiness = wait_socket(listener, true, 200U); + if (readiness == SocketWaitStatus::timeout || + readiness == SocketWaitStatus::interrupted) { + continue; + } + if (readiness != SocketWaitStatus::ready) { + throw std::runtime_error( + "select(listener) failed: " + socket_error_text()); + } + + sockaddr_in peer{}; +#if defined(_WIN32) + int peer_size = static_cast(sizeof(peer)); +#else + socklen_t peer_size = static_cast(sizeof(peer)); +#endif + const auto client = ::accept( + listener, + reinterpret_cast(&peer), + &peer_size); + if (client == kInvalidSocket) { + if (g_stop.load(std::memory_order_relaxed) || socket_interrupted()) { + continue; + } + std::cerr << "accept() failed: " << socket_error_text() << '\n'; + continue; + } + ++connection_count; + std::cout << "CONNECTION_ACCEPTED count=" << connection_count << '\n'; + serve_connection( + client, + object_table, + data_sets, + static_cast(connection_count)); + close_socket(client); + std::cout << "CONNECTION_CLOSED count=" << connection_count << '\n'; + } + + close_socket(listener); + std::cout << "STATIC_IED_SERVER_STOPPED connections=" << connection_count << '\n'; + return 0; + } catch (const std::exception& exception) { + std::cerr << "Static IED server failed: " << exception.what() << '\n'; + return 2; + } +} \ No newline at end of file diff --git a/tools/static_ied_server_sigpipe.cpp b/tools/static_ied_server_sigpipe.cpp new file mode 100644 index 000000000..432c651f8 --- /dev/null +++ b/tools/static_ied_server_sigpipe.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +namespace { + +#if !defined(_WIN32) +struct PosixSigpipeGuard final { + PosixSigpipeGuard() noexcept { + std::signal(SIGPIPE, SIG_IGN); + } +}; + +[[maybe_unused]] const PosixSigpipeGuard kPosixSigpipeGuard{}; +#endif + +} // namespace