From 31af2507fbe201029d438cb06d84c4e6d364ebed Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Thu, 3 Apr 2025 09:14:08 -0500 Subject: [PATCH] Bugfix/secure read range codec (#957) * Secured ReadRange service codecs. Added ReadRange unit testing. Secured ReadRange-ACK handler to enable APDU size checking. --- .github/workflows/bsc-tests-linux.yml | 145 ++--- .github/workflows/bsc-tests-windows.yml | 11 +- .github/workflows/gcc.yml | 24 +- .github/workflows/lint.yml | 26 +- Makefile | 5 + src/bacnet/bacdcode.c | 8 +- src/bacnet/basic/service/h_rr.c | 19 +- src/bacnet/readrange.c | 755 +++++++++++++----------- src/bacnet/readrange.h | 14 +- test/CMakeLists.txt | 1 + test/Makefile | 2 + test/bacnet/readrange/CMakeLists.txt | 47 ++ test/bacnet/readrange/src/main.c | 211 +++++++ 13 files changed, 828 insertions(+), 440 deletions(-) create mode 100644 test/bacnet/readrange/CMakeLists.txt create mode 100644 test/bacnet/readrange/src/main.c diff --git a/.github/workflows/bsc-tests-linux.yml b/.github/workflows/bsc-tests-linux.yml index 691e0421..d35002a6 100644 --- a/.github/workflows/bsc-tests-linux.yml +++ b/.github/workflows/bsc-tests-linux.yml @@ -12,70 +12,81 @@ jobs: job_bsc_tests_linux: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - name: Create Build Environment - run: | - sudo apt-get update -qq - sudo apt-get install -qq libconfig-dev - sudo apt-get install -qq libcap-dev - sudo apt-get install -qq libssl-dev - sudo apt-get install -qq libuv1-dev - git clone --branch v4.3-stable https://github.com/warmcat/libwebsockets.git - bash -c 'cd libwebsockets;mkdir build;cd build;cmake .. -DLWS_WITH_LIBUV=ON -DLWS_WITH_MINIMAL_EXAMPLES=0 -DLWS_MAX_SMP=32;make' - sudo bash -c 'cd libwebsockets;cd build;make install' - - name: Build and run bsc_event test - run: | - cd ./test/ports/linux/bsc_event - mkdir build - cd build - cmake .. - make - ./test_bsc_event - - name: Build and run bvlc-sc test - run: | - cd ./test/bacnet/datalink/bvlc-sc - mkdir build - cd build - cmake .. - make - ./test_bvlc-sc - - name: Build and run websockets test - run: | - cd ./test/bacnet/datalink/websockets - mkdir build - cd build - cmake .. - make - ./test_websockets - - name: Build and run bsc-socket test - run: | - cd ./test/bacnet/datalink/bsc-socket - mkdir build - cd build - cmake .. - make - ./test_bsc-socket - - name: Build and run hub-sc test - run: | - cd ./test/bacnet/datalink/hub-sc - mkdir build - cd build - cmake .. - make - ./test_hub-sc - - name: Build and run bsc-node test - run: | - cd ./test/bacnet/datalink/bsc-node - mkdir build - cd build - cmake .. - make - ./test_bsc-node - - name: Build and run bsc-datalink test - run: | - cd ./test/bacnet/datalink/bsc-datalink - mkdir build - cd build - cmake .. - make - ./test_bsc-datalink \ No newline at end of file + - name: Checkout BACnet Stack + uses: actions/checkout@v4 + - name: Checkout libwebsockets + uses: actions/checkout@v4 + with: + repository: warmcat/libwebsockets + ref: v4.3-stable + path: libwebsockets + - name: Create Build Environment + run: | + sudo apt-get update -qq + sudo apt-get install -qq libconfig-dev + sudo apt-get install -qq libcap-dev + sudo apt-get install -qq libssl-dev + sudo apt-get install -qq libuv1-dev + bash -c 'cd libwebsockets;mkdir build;cd build;cmake .. -DLWS_WITH_LIBUV=ON -DLWS_WITH_MINIMAL_EXAMPLES=0 -DLWS_MAX_SMP=32;make' + sudo bash -c 'cd libwebsockets;cd build;make install' + - name: Build and run bsc_event test + run: | + pwd + cd ./test/ports/linux/bsc_event + mkdir build + cd build + cmake .. + make + ./test_bsc_event + - name: Build and run bvlc-sc test + run: | + pwd + cd ./test/bacnet/datalink/bvlc-sc + mkdir build + cd build + cmake .. + make + ./test_bvlc-sc + - name: Build and run websockets test + run: | + pwd + cd ./test/bacnet/datalink/websockets + mkdir build + cd build + cmake .. + make + ./test_websockets + - name: Build and run bsc-socket test + run: | + pwd + cd ./test/bacnet/datalink/bsc-socket + mkdir build + cd build + cmake .. + make + ./test_bsc-socket + - name: Build and run hub-sc test + run: | + pwd + cd ./test/bacnet/datalink/hub-sc + mkdir build + cd build + cmake .. + make + ./test_hub-sc + - name: Build and run bsc-node test + run: | + cd ./test/bacnet/datalink/bsc-node + mkdir build + cd build + cmake .. + make + ./test_bsc-node + - name: Build and run bsc-datalink test + run: | + cd ./test/bacnet/datalink/bsc-datalink + mkdir build + cd build + cmake .. + make + ./test_bsc-datalink \ No newline at end of file diff --git a/.github/workflows/bsc-tests-windows.yml b/.github/workflows/bsc-tests-windows.yml index 58f7aa77..ee85e330 100644 --- a/.github/workflows/bsc-tests-windows.yml +++ b/.github/workflows/bsc-tests-windows.yml @@ -1,12 +1,7 @@ name: BACNet/SC windows tests on: - push: - branches: - - master - pull_request: - branches: - - '*' + workflow_dispatch: jobs: job_bsc_tests_windows: @@ -35,7 +30,7 @@ jobs: Copy-Item -Path .\pkg\lib -Destination c:\vcpkg\installed\x64-windows-custom -Recurse -Force Copy-Item -Path .\pkg\share -Destination c:\vcpkg\installed\x64-windows-custom -Recurse -Force cd ../../ - - name: Setup MSBuild + - name: Setup MSBuild uses: microsoft/setup-msbuild@v2 with: msbuild-architecture: x64 @@ -63,7 +58,7 @@ jobs: cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=c:\vcpkg\scripts\buildsystems\vcpkg.cmake -DVCPKG_TARGET_TRIPLET=x64-windows-custom msbuild build/test_bsc-socket.sln /Property:Configuration=Release ./build/Release/test_bsc-socket.exe - # (Link target) -> + # (Link target) -> # bacreal.obj : error LNK2019: unresolved external symbol big_endian referenced in function decode_double [D:\a\bacnet-stack-upstream\bacnet-stack-upstream\test\bacnet\datalink\hub-sc\build\test_hub-sc.vcxproj] # - name: Build and run hub-sc test # run: | diff --git a/.github/workflows/gcc.yml b/.github/workflows/gcc.yml index 6e5c4308..c83583f7 100644 --- a/.github/workflows/gcc.yml +++ b/.github/workflows/gcc.yml @@ -80,7 +80,14 @@ jobs: bacnet-sc-hub: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout BACnet Stack + uses: actions/checkout@v4 + - name: Checkout libwebsockets + uses: actions/checkout@v4 + with: + repository: warmcat/libwebsockets + ref: v4.3-stable + path: libwebsockets - name: Create BACnet/SC Build Environment run: | sudo apt-get update -qq @@ -88,19 +95,27 @@ jobs: sudo apt-get install -qq libcap-dev sudo apt-get install -qq libssl-dev sudo apt-get install -qq libuv1-dev - git clone --branch v4.3-stable https://github.com/warmcat/libwebsockets.git bash -c 'cd libwebsockets;mkdir build;cd build;cmake .. -DLWS_WITH_LIBUV=ON -DLWS_WITH_MINIMAL_EXAMPLES=0 -DLWS_MAX_SMP=32;make' sudo bash -c 'cd libwebsockets;cd build;make install' - name: Build BACnet/SC Hub Demo run: | gcc --version + pwd + ls -al make clean make sc-hub router: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout BACnet Stack + uses: actions/checkout@v4 + - name: Checkout libwebsockets + uses: actions/checkout@v4 + with: + repository: warmcat/libwebsockets + ref: v4.3-stable + path: libwebsockets - name: Create Build Environment run: | sudo apt-get update -qq @@ -108,12 +123,13 @@ jobs: sudo apt-get install -qq libcap-dev sudo apt-get install -qq libssl-dev sudo apt-get install -qq libuv1-dev - git clone --branch v4.3-stable https://github.com/warmcat/libwebsockets.git bash -c 'cd libwebsockets;mkdir build;cd build;cmake .. -DLWS_WITH_LIBUV=ON -DLWS_WITH_MINIMAL_EXAMPLES=0 -DLWS_MAX_SMP=32;make' sudo bash -c 'cd libwebsockets;cd build;make install' - name: Build Router Demo run: | gcc --version + pwd + ls -al make clean make LEGACY=true router diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7020623c..5532f533 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -85,14 +85,34 @@ jobs: steps: - uses: actions/checkout@v4 - name: Create Build Environment + run: | + sudo apt-get update -qq + sudo apt-get install -qq lcov + - name: Run Unit Test with Code Coverage + run: make test + + unittest-secure-connect: + runs-on: ubuntu-latest + steps: + - name: Checkout BACnet Stack + uses: actions/checkout@v4 + - name: Checkout libwebsockets + uses: actions/checkout@v4 + with: + repository: warmcat/libwebsockets + ref: v4.3-stable + path: libwebsockets + - name: Create BACnet/SC Build Environment run: | sudo apt-get update -qq sudo apt-get install -qq lcov sudo apt-get install -qq libssl-dev sudo apt-get install -qq libcap-dev sudo apt-get install -qq libuv1-dev - git clone --branch v4.3-stable https://github.com/warmcat/libwebsockets.git bash -c 'cd libwebsockets;mkdir build;cd build;cmake .. -DLWS_WITH_LIBUV=ON -DLWS_WITH_MINIMAL_EXAMPLES=0 -DLWS_MAX_SMP=32;make' sudo bash -c 'cd libwebsockets;cd build;make install' - - name: Run Unit Test with Code Coverage - run: make test + - name: Run BACnet/SC Unit Test with Code Coverage + run: | + pwd + ls -al + echo "make test-bsc" diff --git a/Makefile b/Makefile index 995027af..887b7888 100644 --- a/Makefile +++ b/Makefile @@ -519,6 +519,11 @@ test: retest: $(MAKE) -s -j -C test retest +.PHONY: test-bsc +test-bsc: + $(MAKE) -s -C test clean + $(MAKE) -s -j -C test test-bsc + # Zephyr unit testing with twister # expects zephyr to be installed in ../zephyr in Workspace # expects ZEPHYR_BASE to be set. E.g. source ../zephyr/zephyr-env.sh diff --git a/src/bacnet/bacdcode.c b/src/bacnet/bacdcode.c index cd86c3fd..2a901a7e 100644 --- a/src/bacnet/bacdcode.c +++ b/src/bacnet/bacdcode.c @@ -1517,8 +1517,7 @@ int decode_bitstring( * @param apdu - buffer to hold the bytes * @param apdu_size - number of bytes in the buffer to decode * @param len_value - number of bytes in the unsigned value encoding - * @param value - value to decode into - * + * @param value - value to decode into, or NULL for length checking * @return number of bytes decoded, or zero if errors occur */ int bacnet_bitstring_decode( @@ -1533,7 +1532,7 @@ int bacnet_bitstring_decode( uint32_t bytes_used; /* check to see if the APDU is long enough */ - if (apdu && value && (len_value <= apdu_size)) { + if (apdu && (len_value <= apdu_size)) { /* Init/empty the string. */ bitstring_init(value); if (len_value > 0) { @@ -1544,7 +1543,8 @@ int bacnet_bitstring_decode( /* Copy the bytes in reversed bit order. */ for (i = 0; i < bytes_used; i++) { bitstring_set_octet( - value, (uint8_t)i, byte_reverse_bits(apdu[len++])); + value, (uint8_t)i, byte_reverse_bits(apdu[len])); + len++; } /* Erase the remaining unused bits. */ unused_bits = (uint8_t)(apdu[0] & 0x07); diff --git a/src/bacnet/basic/service/h_rr.c b/src/bacnet/basic/service/h_rr.c index d2aa92db..34d4d0ed 100644 --- a/src/bacnet/basic/service/h_rr.c +++ b/src/bacnet/basic/service/h_rr.c @@ -154,14 +154,16 @@ void handler_read_range( len = Encode_RR_payload(&Temp_Buf[0], &data); if (len >= 0) { /* encode the APDU portion of the packet */ - data.application_data = &Temp_Buf[0]; - data.application_data_len = len; - /* FIXME: probably need a length limitation sent with encode */ - len = rr_ack_encode_apdu( - &Handler_Transmit_Buffer[pdu_len], service_data->invoke_id, - &data); - debug_print("RR: Sending Ack!\n"); - error = false; + len = rr_ack_encode_apdu(NULL, service_data->invoke_id, &data); + if (len < sizeof(Handler_Transmit_Buffer) - pdu_len) { + len = rr_ack_encode_apdu( + &Handler_Transmit_Buffer[pdu_len], + service_data->invoke_id, &data); + debug_print("RR: Sending Ack!\n"); + error = false; + } else { + len = -2; /* too big */ + } } if (error) { if (len == -2) { @@ -182,7 +184,6 @@ void handler_read_range( } } } - pdu_len += len; bytes_sent = datalink_send_pdu( src, &npdu_data, &Handler_Transmit_Buffer[0], pdu_len); diff --git a/src/bacnet/readrange.c b/src/bacnet/readrange.c index a806f8ff..79f5a75c 100644 --- a/src/bacnet/readrange.c +++ b/src/bacnet/readrange.c @@ -205,188 +205,197 @@ int rr_encode_apdu( /** * Decode the received ReadRange request * - * @param apdu Pointer to the APDU buffer. - * @param apdu_len Bytes valid in the APDU buffer. - * @param rrdata Pointer to the data used for encoding. - * - * @return Bytes encoded. + * @param apdu Pointer to the APDU buffer. + * @param apdu_size number of bytes in the APDU buffer. + * @param data Pointer to the data filled while decoding. + * @return Bytes decoded, or #BACNET_STATUS_ERROR */ int rr_decode_service_request( - const uint8_t *apdu, unsigned apdu_len, BACNET_READ_RANGE_DATA *rrdata) + const uint8_t *apdu, unsigned apdu_size, BACNET_READ_RANGE_DATA *data) { - unsigned len = 0; - unsigned TagLen = 0; - uint8_t tag_number = 0; - uint32_t len_value_type = 0; - BACNET_OBJECT_TYPE type = OBJECT_NONE; /* for decoding */ - uint32_t enum_value; - BACNET_UNSIGNED_INTEGER unsigned_value; + int len = 0, apdu_len = 0; + uint32_t value32 = 0; + int32_t signed_value = 0; + BACNET_OBJECT_TYPE object_type = OBJECT_NONE; + uint32_t enum_value = 0; + BACNET_UNSIGNED_INTEGER unsigned_value = 0; + BACNET_DATE *bdate = NULL; + BACNET_TIME *btime = NULL; /* check for value pointers */ - if ((apdu_len >= 5) && apdu && rrdata) { - /* Tag 0: Object ID */ - if (!decode_is_context_tag(&apdu[len++], 0)) { - return -1; - } - len += decode_object_id(&apdu[len], &type, &rrdata->object_instance); - rrdata->object_type = type; - /* Tag 1: Property ID */ - if (len >= apdu_len) { - return (-1); - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (tag_number != 1) { - return -1; - } - len += decode_enumerated(&apdu[len], len_value_type, &enum_value); - rrdata->object_property = (BACNET_PROPERTY_ID)enum_value; - rrdata->Overhead = RR_OVERHEAD; /* Start with the fixed overhead */ - - /* Tag 2: Optional Array Index - set to ALL if not present */ - rrdata->array_index = BACNET_ARRAY_ALL; /* Assuming this is the most - common outcome... */ - if (len < apdu_len) { - TagLen = (unsigned)decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (tag_number == 2) { - len += TagLen; - len += decode_unsigned( - &apdu[len], len_value_type, &unsigned_value); - rrdata->array_index = (BACNET_ARRAY_INDEX)unsigned_value; - rrdata->Overhead += - RR_INDEX_OVERHEAD; /* Allow for this in the response */ - } - } - /* And/or optional range selection- Tags 3, 6 and 7 */ - rrdata->RequestType = RR_READ_ALL; /* Assume the worst to cut out - explicit checking later */ - if (len < apdu_len) { - /* - * Note: We pick up the opening tag and then decode the - * parameter types we recognise. We deal with the count and the - * closing tag in each case statement even though it might - * appear that we could do them after the switch statement as - * common elements. This is so that if we receive a tag we don't - * recognise, we don't try to decode it blindly and make a mess - * of it. - */ - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - switch (tag_number) { - case 3: /* ReadRange by position */ - rrdata->RequestType = RR_BY_POSITION; - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (len >= apdu_len) { - break; - } - len += decode_unsigned( - &apdu[len], len_value_type, &unsigned_value); - rrdata->Range.RefIndex = (uint32_t)unsigned_value; - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (len >= apdu_len) { - break; - } - len += decode_signed( - &apdu[len], len_value_type, &rrdata->Count); - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - break; - - case 6: /* ReadRange by sequence number */ - rrdata->RequestType = RR_BY_SEQUENCE; - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (len >= apdu_len) { - break; - } - len += decode_unsigned( - &apdu[len], len_value_type, &unsigned_value); - rrdata->Range.RefSeqNum = (uint32_t)unsigned_value; - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (len >= apdu_len) { - break; - } - len += decode_signed( - &apdu[len], len_value_type, &rrdata->Count); - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - /* Allow for this in the response */ - rrdata->Overhead += RR_1ST_SEQ_OVERHEAD; - break; - - case 7: /* ReadRange by time stamp */ - rrdata->RequestType = RR_BY_TIME; - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (len >= apdu_len) { - break; - } - len += decode_date(&apdu[len], &rrdata->Range.RefTime.date); - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (len >= apdu_len) { - break; - } - len += decode_bacnet_time( - &apdu[len], &rrdata->Range.RefTime.time); - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (len >= apdu_len) { - break; - } - len += decode_signed( - &apdu[len], len_value_type, &rrdata->Count); - if (len >= apdu_len) { - break; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - /* Allow for this in the response */ - rrdata->Overhead += RR_1ST_SEQ_OVERHEAD; - break; - - default: /* If we don't recognise the tag then we do nothing - * here and try to return all elements of the array - */ - break; - } + if (!apdu) { + return BACNET_STATUS_ERROR; + } + /* objectIdentifier [0] BACnetObjectIdentifier */ + len = bacnet_object_id_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, &object_type, &value32); + if (len > 0) { + apdu_len += len; + if (data) { + data->object_type = object_type; + data->object_instance = value32; } } else { - return (-1); + return BACNET_STATUS_ERROR; + } + /* propertyIdentifier [1] BACnetPropertyIdentifier */ + len = bacnet_enumerated_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 1, &enum_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->object_property = (BACNET_PROPERTY_ID)enum_value; + data->Overhead = RR_OVERHEAD; /* Start with the fixed overhead */ + } + } else { + return BACNET_STATUS_ERROR; + } + /* propertyArrayIndex [2] Unsigned OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 2, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->array_index = (BACNET_ARRAY_INDEX)unsigned_value; + data->Overhead += RR_INDEX_OVERHEAD; + } + } else if (len == 0) { + /* OPTIONAL missing - skip adding len */ + if (data) { + data->array_index = BACNET_ARRAY_ALL; + } + } else { + return BACNET_STATUS_ERROR; + } + if (bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 3, &len)) { + /* + byPosition [3] SEQUENCE { + referenceIndex Unsigned, + count INTEGER + } + */ + apdu_len += len; + if (data) { + data->RequestType = RR_BY_POSITION; + } + len = bacnet_unsigned_application_decode( + &apdu[apdu_len], apdu_size - apdu_len, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->Range.RefIndex = (uint32_t)unsigned_value; + } + } else { + return BACNET_STATUS_ERROR; + } + len = bacnet_signed_application_decode( + &apdu[apdu_len], apdu_size - apdu_len, &signed_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->Count = signed_value; + } + } else { + return BACNET_STATUS_ERROR; + } + if (bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 3, &len)) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + } else if (bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 6, &len)) { + /* + bySequenceNumber [6] SEQUENCE { + referenceIndex Unsigned, + count INTEGER + } + */ + apdu_len += len; + if (data) { + data->RequestType = RR_BY_SEQUENCE; + } + len = bacnet_unsigned_application_decode( + &apdu[apdu_len], apdu_size - apdu_len, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->Range.RefSeqNum = (uint32_t)unsigned_value; + data->Overhead += RR_1ST_SEQ_OVERHEAD; + } + } else { + return BACNET_STATUS_ERROR; + } + len = bacnet_signed_application_decode( + &apdu[apdu_len], apdu_size - apdu_len, &signed_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->Count = signed_value; + } + } else { + return BACNET_STATUS_ERROR; + } + if (bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 6, &len)) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + } else if (bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 7, &len)) { + /* + byTime [7] SEQUENCE { + referenceTime BACnetDateTime, + count INTEGER + } + */ + apdu_len += len; + if (data) { + data->RequestType = RR_BY_TIME; + bdate = &data->Range.RefTime.date; + btime = &data->Range.RefTime.time; + } + len = bacnet_date_application_decode( + &apdu[apdu_len], apdu_size - apdu_len, bdate); + if (len > 0) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + len = bacnet_time_application_decode( + &apdu[apdu_len], apdu_size - apdu_len, btime); + if (len > 0) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + len = bacnet_signed_application_decode( + &apdu[apdu_len], apdu_size - apdu_len, &signed_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->Count = signed_value; + } + } else { + return BACNET_STATUS_ERROR; + } + if (bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 7, &len)) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + } else { + /* OPTIONAL range missing - skip adding len */ + if (data) { + data->RequestType = RR_READ_ALL; + } } - return (int)len; + return apdu_len; } /* @@ -404,70 +413,134 @@ int rr_decode_service_request( * } */ +/** + * @brief Encode ReadRange-ACK service APDU + * @param apdu Pointer to the buffer, or NULL for length + * @param data Pointer to the data to encode. + * @return number of bytes encoded, or zero on error. + */ +int readrange_ack_encode(uint8_t *apdu, const BACNET_READ_RANGE_DATA *data) +{ + int apdu_len = 0; /* total length of the apdu, return value */ + int len = 0; + + if (!data) { + return 0; + } + len = encode_context_object_id( + apdu, 0, data->object_type, data->object_instance); + apdu_len += len; + if (apdu) { + apdu += len; + } + len = encode_context_enumerated(apdu, 1, data->object_property); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* context 2 array index is optional */ + if (data->array_index != BACNET_ARRAY_ALL) { + len = encode_context_unsigned(apdu, 2, data->array_index); + apdu_len += len; + if (apdu) { + apdu += len; + } + } + /* Context 3 BACnet Result Flags */ + len = encode_context_bitstring(apdu, 3, &data->ResultFlags); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* Context 4 Item Count */ + len = encode_context_unsigned(apdu, 4, data->ItemCount); + apdu_len += len; + if (apdu) { + apdu += len; + } + /* Context 5 Property list - reading the standard it looks like an + * empty list still requires an opening and closing tag as the + * tagged parameter is not optional + */ + len = encode_opening_tag(apdu, 5); + apdu_len += len; + if (apdu) { + apdu += len; + } + if (data->application_data_len > 0) { + for (len = 0; len < data->application_data_len; len++) { + if (apdu) { + apdu[len] = data->application_data[len]; + } + } + apdu_len += len; + if (apdu) { + apdu += len; + } + } + len = encode_closing_tag(apdu, 5); + apdu_len += len; + if (apdu) { + apdu += len; + } + if ((data->ItemCount != 0) && (data->RequestType != RR_BY_POSITION) && + (data->RequestType != RR_READ_ALL)) { + /* Context 6 Sequence number of first item */ + len = encode_context_unsigned(apdu, 6, data->FirstSequence); + apdu_len += len; + } + + return apdu_len; +} + +/** + * @brief Encode the ReadRange-ACK service + * @param apdu Pointer to the buffer for encoding into, or NULL for length + * @param apdu_size number of bytes available in the buffer + * @param data Pointer to the service data to be encoded + * @return number of bytes encoded, or zero if unable to encode or too large + */ +size_t readrange_ack_service_encode( + uint8_t *apdu, size_t apdu_size, const BACNET_READ_RANGE_DATA *data) +{ + size_t apdu_len = 0; /* total length of the apdu, return value */ + + apdu_len = readrange_ack_encode(NULL, data); + if (apdu_len > apdu_size) { + apdu_len = 0; + } else { + apdu_len = readrange_ack_encode(apdu, data); + } + + return apdu_len; +} + /** * Build a ReadRange response packet * * @param apdu Pointer to the buffer. - * @param invoke_id ID invoked. - * @param rrdata Pointer to the read range data structure used for - * encoding. - * - * @return The count of encoded bytes. + * @param invoke_id original invoke id for request + * @param data Pointer to the property data to be encoded + * @return number of bytes encoded */ int rr_ack_encode_apdu( - uint8_t *apdu, uint8_t invoke_id, const BACNET_READ_RANGE_DATA *rrdata) + uint8_t *apdu, uint8_t invoke_id, const BACNET_READ_RANGE_DATA *data) { - int imax = 0; - int len = 0; /* length of each encoding */ int apdu_len = 0; /* total length of the apdu, return value */ + int len = 0; if (apdu) { apdu[0] = PDU_TYPE_COMPLEX_ACK; /* complex ACK service */ apdu[1] = invoke_id; /* original invoke id from request */ apdu[2] = SERVICE_CONFIRMED_READ_RANGE; /* service choice */ - apdu_len = 3; - /* service ack follows */ - apdu_len += encode_context_object_id( - &apdu[apdu_len], 0, rrdata->object_type, rrdata->object_instance); - apdu_len += encode_context_enumerated( - &apdu[apdu_len], 1, rrdata->object_property); - /* context 2 array index is optional */ - if (rrdata->array_index != BACNET_ARRAY_ALL) { - apdu_len += encode_context_unsigned( - &apdu[apdu_len], 2, rrdata->array_index); - } - /* Context 3 BACnet Result Flags */ - apdu_len += - encode_context_bitstring(&apdu[apdu_len], 3, &rrdata->ResultFlags); - /* Context 4 Item Count */ - apdu_len += - encode_context_unsigned(&apdu[apdu_len], 4, rrdata->ItemCount); - /* Context 5 Property list - reading the standard it looks like an - * empty list still requires an opening and closing tag as the - * tagged parameter is not optional - */ - apdu_len += encode_opening_tag(&apdu[apdu_len], 5); - if (rrdata->ItemCount != 0) { - imax = rrdata->application_data_len; - if (imax > (MAX_APDU - apdu_len - 2 /*closing*/)) { - imax = (MAX_APDU - apdu_len - 2); - } - for (len = 0; len < imax; len++) { - apdu[apdu_len++] = rrdata->application_data[len]; - } - } - apdu_len += encode_closing_tag(&apdu[apdu_len], 5); - - if ((rrdata->ItemCount != 0) && - (rrdata->RequestType != RR_BY_POSITION) && - (rrdata->RequestType != RR_READ_ALL)) { - /* Context 6 Sequence number of first item */ - if (apdu_len < (MAX_APDU - 4)) { - apdu_len += encode_context_unsigned( - &apdu[apdu_len], 6, rrdata->FirstSequence); - } - } } + len = 3; + apdu_len += len; + if (apdu) { + apdu += len; + } + len = readrange_ack_encode(apdu, data); + apdu_len += len; return apdu_len; } @@ -475,137 +548,139 @@ int rr_ack_encode_apdu( /** * Decode the received ReadRange response * - * @param apdu Pointer to the APDU buffer. - * @param apdu_len Bytes valid in the APDU buffer. - * @param rrdata Pointer to the data filled while decoding. - * - * @return Bytes decoded. + * @param apdu Pointer to the APDU buffer. + * @param apdu_size Number of bytes in the APDU buffer. + * @param data Pointer to the data filled while decoding (can be NULL). + * @return number of bytes decoded, or #BACNET_STATUS_ERROR */ int rr_ack_decode_service_request( - uint8_t *apdu, - int apdu_len, /* total length of the apdu */ - BACNET_READ_RANGE_DATA *rrdata) + uint8_t *apdu, int apdu_size, BACNET_READ_RANGE_DATA *data) { - uint8_t tag_number = 0; - uint32_t len_value_type = 0; - int tag_len = 0; /* length of tag decode */ - int len = 0; /* total length of decodes */ - int start_len; - BACNET_OBJECT_TYPE object_type = OBJECT_NONE; /* object type */ - uint32_t property = 0; /* for decoding */ + int apdu_len = 0; + int len = 0; + int data_len = 0; + BACNET_OBJECT_TYPE object_type = OBJECT_NONE; + uint32_t value32 = 0; BACNET_UNSIGNED_INTEGER unsigned_value; + BACNET_BIT_STRING *bitstring = NULL; /* Check apdu_len against the len during decode. */ - if (apdu && (apdu_len >= 5 /* minimum */)) { - /* Tag 0: Object ID */ - if (!decode_is_context_tag(&apdu[0], 0)) { - return -1; + if (!apdu) { + return BACNET_STATUS_ERROR; + } + /* objectIdentifier [0] BACnetObjectIdentifier */ + len = bacnet_object_id_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 0, &object_type, &value32); + if (len > 0) { + apdu_len += len; + if (data) { + data->object_type = object_type; + data->object_instance = value32; } - len = 1; - len += decode_object_id( - &apdu[len], &object_type, &rrdata->object_instance); - rrdata->object_type = object_type; - - /* Tag 1: Property ID */ - if (len >= apdu_len) { - return -1; + } else { + return BACNET_STATUS_ERROR; + } + /* propertyIdentifier [1] BACnetPropertyIdentifier */ + len = bacnet_enumerated_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 1, &value32); + if (len > 0) { + apdu_len += len; + if (data) { + data->object_property = (BACNET_PROPERTY_ID)value32; } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (tag_number != 1) { - return -1; + } else { + return BACNET_STATUS_ERROR; + } + /* propertyArrayIndex [2] Unsigned OPTIONAL */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 2, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->array_index = (BACNET_ARRAY_INDEX)unsigned_value; } - len += decode_enumerated(&apdu[len], len_value_type, &property); - rrdata->object_property = (BACNET_PROPERTY_ID)property; - - /* Tag 2: Optional Array Index */ - if (len >= apdu_len) { - return -1; + } else if (len == 0) { + /* OPTIONAL missing - skip adding len */ + if (data) { + data->array_index = BACNET_ARRAY_ALL; } - tag_len = decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (tag_number == 2) { - len += tag_len; - len += decode_unsigned(&apdu[len], len_value_type, &unsigned_value); - rrdata->array_index = (BACNET_ARRAY_INDEX)unsigned_value; - } else { - rrdata->array_index = BACNET_ARRAY_ALL; + } else { + return BACNET_STATUS_ERROR; + } + /* resultFlags [3] BACnetResultFlags */ + if (data) { + bitstring = &data->ResultFlags; + } + len = bacnet_bitstring_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 3, bitstring); + if (len > 0) { + apdu_len += len; + } else { + return BACNET_STATUS_ERROR; + } + /* itemCount [4] Unsigned */ + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 4, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->ItemCount = (uint32_t)unsigned_value; } - - /* Tag 3: Result Flags */ - if (len >= apdu_len) { - return -1; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (tag_number != 3) { - return -1; - } - if (len >= apdu_len) { - return -1; - } - len += - decode_bitstring(&apdu[len], len_value_type, &rrdata->ResultFlags); - - /* Tag 4: Item count */ - if (len >= apdu_len) { - return -1; - } - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (tag_number != 4) { - return -1; - } - if (len >= apdu_len) { - return -1; - } - len += decode_unsigned(&apdu[len], len_value_type, &unsigned_value); - rrdata->ItemCount = (uint32_t)unsigned_value; - if (len >= apdu_len) { - return -1; - } - if (decode_is_opening_tag_number(&apdu[len], 5)) { - len++; /* A tag number of 5 is not extended so only one octet - * Setup the start position and length of the data - * returned from the request don't decode the application - * tag number or its data here. */ - rrdata->application_data = &apdu[len]; - start_len = len; - while (len < apdu_len) { - if (IS_CONTEXT_SPECIFIC(apdu[len]) && - (decode_is_closing_tag_number(&apdu[len], 5))) { - rrdata->application_data_len = len - start_len; - len++; /* Step over single byte closing tag */ - break; - } else { - /* Don't care about tag number, just skipping over - * anyway */ - len += decode_tag_number_and_value( - &apdu[len], NULL, &len_value_type); - len += len_value_type; /* Skip over data value as well */ - if (len >= apdu_len) { /* APDU is exhausted so we have - * failed to find closing tag */ - return (-1); - } - } + } else { + return BACNET_STATUS_ERROR; + } + /* itemData [5] SEQUENCE OF ABSTRACT-SYNTAX.&TYPE */ + if (!bacnet_is_opening_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 5, &len)) { + return BACNET_STATUS_ERROR; + } + /* determine the length of the data blob + note: APDU must include the opening tag in order to find + the matching closing tag */ + data_len = + bacnet_enclosed_data_length(&apdu[apdu_len], apdu_size - apdu_len); + if (data_len == BACNET_STATUS_ERROR) { + return BACNET_STATUS_ERROR; + } + /* count the opening tag number length AFTER getting the data length */ + apdu_len += len; + /* sanity check */ + if (data_len > MAX_APDU) { + /* not enough size in application_data to store the data chunk */ + return BACNET_STATUS_ERROR; + } else if (data) { + /* don't decode the application tag number or its data here */ + data->application_data = &apdu[apdu_len]; + data->application_data_len = data_len; + } + apdu_len += data_len; + if (!bacnet_is_closing_tag_number( + &apdu[apdu_len], apdu_size - apdu_len, 5, &len)) { + return BACNET_STATUS_ERROR; + } + /* count the closing tag number length */ + apdu_len += len; + /* firstSequenceNumber [6] Unsigned32 OPTIONAL + -- used only if 'Item Count' > 0 and + -- the request was either of type 'By Sequence Number' + -- or 'By Time' */ + if (apdu_len < apdu_size) { + len = bacnet_unsigned_context_decode( + &apdu[apdu_len], apdu_size - apdu_len, 6, &unsigned_value); + if (len > 0) { + apdu_len += len; + if (data) { + data->FirstSequence = (uint32_t)unsigned_value; + } + } else if (len == 0) { + /* OPTIONAL missing - skip adding len */ + if (data) { + data->FirstSequence = 0; } } else { - return -1; - } - if (len < apdu_len) { /* Still something left to look at? */ - /* Tag 6: FirstSequence */ - len += decode_tag_number_and_value( - &apdu[len], &tag_number, &len_value_type); - if (tag_number != 6) { - return -1; - } - if (len < apdu_len) { - len += decode_unsigned( - &apdu[len], len_value_type, &unsigned_value); - rrdata->FirstSequence = (uint32_t)unsigned_value; - } + return BACNET_STATUS_ERROR; } } - return len; + return apdu_len; } diff --git a/src/bacnet/readrange.h b/src/bacnet/readrange.h index 35b587ec..e7f163f6 100644 --- a/src/bacnet/readrange.h +++ b/src/bacnet/readrange.h @@ -44,14 +44,13 @@ typedef struct BACnet_Read_Range_Data { /** Defines to indicate which type of read range request it is. Not really a bit map but we do it like this to allow quick checking of request against capabilities for the property */ - #define RR_BY_POSITION 1 #define RR_BY_SEQUENCE 2 #define RR_BY_TIME 4 -#define RR_READ_ALL \ - 8 /**< Read all of array - so don't send any range in the request */ -#define RR_ARRAY_OF_LISTS \ - 16 /**< For info functionality indicates array of lists if set */ +/**< Read all of the list, and don't encode OPTIONAL range in the request */ +#define RR_READ_ALL 8 +/**< For info functionality indicates array of lists if set */ +#define RR_ARRAY_OF_LISTS 16 /** Bit String Enumerations */ typedef enum { @@ -135,6 +134,11 @@ int rr_decode_service_request( BACNET_STACK_EXPORT int rr_ack_encode_apdu( uint8_t *apdu, uint8_t invoke_id, const BACNET_READ_RANGE_DATA *rrdata); +BACNET_STACK_EXPORT +int readrange_ack_encode(uint8_t *apdu, const BACNET_READ_RANGE_DATA *data); +BACNET_STACK_EXPORT +size_t readrange_ack_service_encode( + uint8_t *apdu, size_t apdu_size, const BACNET_READ_RANGE_DATA *data); BACNET_STACK_EXPORT int rr_ack_decode_service_request( diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a9dae6dc..a531e6c5 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -107,6 +107,7 @@ list(APPEND testdirs bacnet/property bacnet/ptransfer bacnet/rd + bacnet/readrange bacnet/reject bacnet/rp bacnet/rpm diff --git a/test/Makefile b/test/Makefile index d292c54e..f49ed8e7 100644 --- a/test/Makefile +++ b/test/Makefile @@ -140,6 +140,8 @@ libwebsockets: bash -c 'cd libwebsockets;mkdir build;cd build;cmake .. -DLWS_WITH_LIBUV=ON -DLWS_WITH_MINIMAL_EXAMPLES=0 -DLWS_MAX_SMP=32;make' sudo bash -c 'cd libwebsockets;cd build;make install' +test-bsc: bsc-datalink bsc-node bsc-hub bsc-bvlc bsc-socket websockets + .PHONY: clean clean: -rm -rf $(BUILD_DIR) diff --git a/test/bacnet/readrange/CMakeLists.txt b/test/bacnet/readrange/CMakeLists.txt new file mode 100644 index 00000000..0f604ec1 --- /dev/null +++ b/test/bacnet/readrange/CMakeLists.txt @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: MIT + +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) + +get_filename_component(basename ${CMAKE_CURRENT_SOURCE_DIR} NAME) +project(test_${basename} + VERSION 1.0.0 + LANGUAGES C) + + +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/src" + SRC_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +string(REGEX REPLACE + "/test/bacnet/[a-zA-Z_/-]*$" + "/test" + TST_DIR + ${CMAKE_CURRENT_SOURCE_DIR}) +set(ZTST_DIR "${TST_DIR}/ztest/src") + +add_compile_definitions( + BIG_ENDIAN=0 + CONFIG_ZTEST=1 + ) + +include_directories( + ${SRC_DIR} + ${TST_DIR}/ztest/include + ) + +add_executable(${PROJECT_NAME} + # File(s) under test + ${SRC_DIR}/bacnet/readrange.c + # Support files and stubs (pathname alphabetical) + ${SRC_DIR}/bacnet/bacdcode.c + ${SRC_DIR}/bacnet/bacint.c + ${SRC_DIR}/bacnet/bacreal.c + ${SRC_DIR}/bacnet/bacstr.c + ${SRC_DIR}/bacnet/proplist.c + ${SRC_DIR}/bacnet/basic/sys/bigend.c + # Test and test library files + ./src/main.c + ${ZTST_DIR}/ztest_mock.c + ${ZTST_DIR}/ztest.c + ) diff --git a/test/bacnet/readrange/src/main.c b/test/bacnet/readrange/src/main.c new file mode 100644 index 00000000..eacfc0c8 --- /dev/null +++ b/test/bacnet/readrange/src/main.c @@ -0,0 +1,211 @@ +/** + * @file + * @brief Unit test for ReadRange services encode and decode + * @author Steve Karg + * @date April 2025 + * @copyright SPDX-License-Identifier: MIT + */ +#include +#include +#include + +/** + * @addtogroup bacnet_tests + * @{ + */ + +static const char *read_range_request_type(int type) +{ + switch (type) { + case RR_BY_POSITION: + return "RR_BY_POSITION"; + case RR_BY_SEQUENCE: + return "RR_BY_SEQUENCE"; + case RR_BY_TIME: + return "RR_BY_TIME"; + case RR_READ_ALL: + return "RR_READ_ALL"; + default: + return "UNKNOWN"; + } +} + +/** + * @brief Test + */ +static void testReadRangeAckUnit(BACNET_READ_RANGE_DATA *data) +{ + uint8_t apdu[480] = { 0 }; + uint8_t apdu2[480] = { 0 }; + int apdu_len = 0, test_len = 0, null_len = 0; + BACNET_READ_RANGE_DATA test_data = { 0 }; + BACNET_OBJECT_TYPE object_type = OBJECT_DEVICE; + uint32_t object_instance = 0; + BACNET_OBJECT_TYPE object = 0; + + data->application_data_len = encode_bacnet_object_id( + &apdu2[0], data->object_type, data->object_instance); + data->application_data = &apdu2[0]; + + null_len = readrange_ack_service_encode(&apdu[0], sizeof(apdu), NULL); + zassert_equal(null_len, 0, NULL); + null_len = readrange_ack_service_encode(&apdu[0], 0, NULL); + zassert_equal(null_len, 0, NULL); + null_len = readrange_ack_service_encode(NULL, sizeof(apdu), data); + zassert_not_equal(null_len, 0, NULL); + apdu_len = readrange_ack_service_encode(&apdu[0], sizeof(apdu), data); + zassert_equal( + apdu_len, null_len, "apdu_len=%d null_len=%d", apdu_len, null_len); + zassert_not_equal(apdu_len, 0, NULL); + zassert_not_equal(apdu_len, BACNET_STATUS_ERROR, NULL); + null_len = rr_ack_decode_service_request(NULL, apdu_len, &test_data); + zassert_true(null_len < 0, NULL); + test_len = rr_ack_decode_service_request(&apdu[0], apdu_len, &test_data); + zassert_equal(apdu_len, test_len, NULL); + zassert_not_equal(test_len, -1, NULL); + + zassert_equal(test_data.object_type, data->object_type, NULL); + zassert_equal(test_data.object_instance, data->object_instance, NULL); + zassert_equal(test_data.object_property, data->object_property, NULL); + zassert_equal(test_data.array_index, data->array_index, NULL); + zassert_equal( + test_data.application_data_len, data->application_data_len, + "test app len=%d app len=%d", test_data.application_data_len, + data->application_data_len); + + /* since object property == object_id, decode the application data using + the appropriate decode function */ + test_len = + decode_object_id(test_data.application_data, &object, &object_instance); + object_type = object; + zassert_equal(object_type, data->object_type, NULL); + zassert_equal(object_instance, data->object_instance, NULL); + while (apdu_len) { + apdu_len--; + if (apdu_len == 17) { + /* boundary of optional parameters, so becomes valid */ + continue; + } + test_len = + rr_ack_decode_service_request(&apdu[0], apdu_len, &test_data); + zassert_true( + test_len < 0, "test_len=%d apdu_len=%d", test_len, apdu_len); + } +} + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(rp_tests, testReadRangeAck) +#else +static void testReadRangeAck(void) +#endif +{ + BACNET_READ_RANGE_DATA data = { 0 }; + + data.object_type = OBJECT_DEVICE; + data.object_instance = 1; + data.object_property = PROP_OBJECT_IDENTIFIER; + data.array_index = 0; + data.RequestType = RR_READ_ALL; + testReadRangeAckUnit(&data); + data.array_index = BACNET_ARRAY_ALL; + for (int i = 0; i < 3; i++) { + data.ItemCount = i; + data.RequestType = RR_READ_ALL; + testReadRangeAckUnit(&data); + /* firstSequenceNumber - used only if 'Item Count' > 0 and + the request was either of type 'By Sequence Number' or 'By Time' */ + for (int j = 0; j < 3; j++) { + data.FirstSequence = j; + data.RequestType = RR_BY_TIME; + testReadRangeAckUnit(&data); + data.RequestType = RR_BY_SEQUENCE; + testReadRangeAckUnit(&data); + } + data.FirstSequence = 0; + data.RequestType = RR_BY_POSITION; + testReadRangeAckUnit(&data); + } +} + +static void testReadRangeUnit(BACNET_READ_RANGE_DATA *data) +{ + uint8_t apdu[480] = { 0 }; + int apdu_len = 0, test_len = 0, null_len = 0; + BACNET_READ_RANGE_DATA test_data; + + null_len = read_range_request_encode(&apdu[0], 0, data); + zassert_equal(null_len, 0, NULL); + null_len = read_range_request_encode(&apdu[0], sizeof(apdu), NULL); + zassert_equal(null_len, 0, NULL); + null_len = read_range_request_encode(NULL, sizeof(apdu), data); + zassert_not_equal(null_len, 0, NULL); + apdu_len = read_range_request_encode(&apdu[0], sizeof(apdu), data); + zassert_equal( + apdu_len, null_len, "apdu_len=%d null_len=%d", apdu_len, null_len); + zassert_not_equal(apdu_len, 0, NULL); + null_len = rr_decode_service_request(NULL, apdu_len, &test_data); + zassert_true(null_len < 0, NULL); + test_len = rr_decode_service_request(&apdu[0], apdu_len, &test_data); + zassert_equal( + apdu_len, test_len, "apdu_len=%d test_len=%d", apdu_len, test_len); + zassert_not_equal(test_len, -1, NULL); + zassert_equal(test_data.object_type, data->object_type, NULL); + zassert_equal(test_data.object_instance, data->object_instance, NULL); + zassert_equal(test_data.object_property, data->object_property, NULL); + zassert_equal(test_data.array_index, data->array_index, NULL); + while (apdu_len) { + apdu_len--; + test_len = rr_decode_service_request(&apdu[0], apdu_len, &test_data); + if (apdu_len == 7) { + /* boundary of optional parameters, so becomes valid */ + continue; + } + zassert_true( + test_len < 0, "test_len=%d apdu_len=%d request=%s array=%u", + test_len, apdu_len, read_range_request_type(data->RequestType), + data->array_index); + } +} + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(rp_tests, testReadRange) +#else +static void testReadRange(void) +#endif +{ + BACNET_READ_RANGE_DATA data = { 0 }; + + data.object_type = OBJECT_DEVICE; + data.object_instance = 1; + data.object_property = PROP_OBJECT_IDENTIFIER; + data.array_index = 0; + data.RequestType = RR_READ_ALL; + testReadRangeUnit(&data); + data.array_index = BACNET_ARRAY_ALL; + data.RequestType = RR_READ_ALL; + testReadRangeUnit(&data); + data.RequestType = RR_BY_POSITION; + testReadRangeUnit(&data); + data.RequestType = RR_BY_SEQUENCE; + testReadRangeUnit(&data); + data.RequestType = RR_BY_TIME; + testReadRangeUnit(&data); + + return; +} +/** + * @} + */ + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST_SUITE(rp_tests, NULL, NULL, NULL, NULL, NULL); +#else +void test_main(void) +{ + ztest_test_suite( + readrange_tests, ztest_unit_test(testReadRange), + ztest_unit_test(testReadRangeAck)); + + ztest_run_test_suite(readrange_tests); +} +#endif