From a755a8db4d86af71d04d3b1b195c5dbcf86aa24f Mon Sep 17 00:00:00 2001 From: Steve Karg Date: Mon, 16 Mar 2026 17:04:23 -0500 Subject: [PATCH] Feature/octetstring value and length api (#1264) * feat: add octetstring_length_value_same function for length and content comparison * feat: add tests for characterstring UTF-8 validation * feat: add tests for octetstring_init_ascii_epics validation * feat: add test for bacnet_strdup string duplication validation * feat: add utf8_isvalid test cases * feat: add octet string value API for present-value octet-string value and length, and out-of-service properties --- CHANGELOG.md | 7 +- src/bacnet/bacstr.c | 50 +++-- src/bacnet/bacstr.h | 6 +- src/bacnet/basic/object/osv.c | 152 +++++++++++--- src/bacnet/basic/object/osv.h | 23 +- test/bacnet/bacstr/src/main.c | 384 +++++++++++++++++++++++++++++++++- 6 files changed, 563 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f30110fa..08d954d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ The git repositories are hosted at the following sites: * * -## [Unreleased] - 2026-03-12 +## [Unreleased] - 2026-03-16 ### Security @@ -53,6 +53,8 @@ The git repositories are hosted at the following sites: ### Added +* Added octetstring_length_value_same() API for comparing an OctetString + to value and len parameters. Added verification tests. (#1264) * Added property_list_read_only_member function to check for READ-ONLY properties. (#1258) * Added WriteProperty support in the basic Structured View object. @@ -160,6 +162,9 @@ The git repositories are hosted at the following sites: ### Changed +* Changed the OctetString Value object present-value get function + to perform a copy return rather than a pointer return and deprecate + the pointer return function. (#1264) * Changed the COV FSM handler to remiain in the IDLE state until there is a valid subscriber. (#1257) * Changed bacnet_array_write() write_function callback API by adding diff --git a/src/bacnet/bacstr.c b/src/bacnet/bacstr.c index 81135d7d..1cab8a8f 100644 --- a/src/bacnet/bacstr.c +++ b/src/bacnet/bacstr.c @@ -1324,7 +1324,38 @@ size_t octetstring_capacity(const BACNET_OCTET_STRING *octet_string) } /** - * @brief Returns true if the same length and contents. + * @brief Returns true if the same length and value contents. + * + * @param octet_string1 Pointer to the first octet string. + * @param length Length of the second octet string. + * @param value Pointer to the value of the second octet string. + * + * @return true if the octet strings are the same, false otherwise. + */ +bool octetstring_length_value_same( + const BACNET_OCTET_STRING *octet_string1, + size_t length, + const uint8_t *value) +{ + size_t i = 0; /* loop counter */ + + if (octet_string1 && value) { + if ((octet_string1->length == length) && + (octet_string1->length <= MAX_OCTET_STRING_BYTES)) { + for (i = 0; i < octet_string1->length; i++) { + if (octet_string1->value[i] != value[i]) { + return false; + } + } + return true; + } + } + + return false; +} + +/** + * @brief Returns true if the same length and value contents. * * @param octet_string1 Pointer to the first octet string. * @param octet_string2 Pointer to the second octet string. @@ -1335,21 +1366,14 @@ bool octetstring_value_same( const BACNET_OCTET_STRING *octet_string1, const BACNET_OCTET_STRING *octet_string2) { - size_t i = 0; /* loop counter */ + bool status = false; - if (octet_string1 && octet_string2) { - if ((octet_string1->length == octet_string2->length) && - (octet_string1->length <= MAX_OCTET_STRING_BYTES)) { - for (i = 0; i < octet_string1->length; i++) { - if (octet_string1->value[i] != octet_string2->value[i]) { - return false; - } - } - return true; - } + if (octet_string2) { + status = octetstring_length_value_same( + octet_string1, octet_string2->length, octet_string2->value); } - return false; + return status; } #endif diff --git a/src/bacnet/bacstr.h b/src/bacnet/bacstr.h index b70d4c32..1e15615e 100644 --- a/src/bacnet/bacstr.h +++ b/src/bacnet/bacstr.h @@ -173,7 +173,11 @@ BACNET_STACK_EXPORT size_t octetstring_length(const BACNET_OCTET_STRING *octet_string); BACNET_STACK_EXPORT size_t octetstring_capacity(const BACNET_OCTET_STRING *octet_string); -/* returns true if the same length and contents */ +BACNET_STACK_EXPORT +bool octetstring_length_value_same( + const BACNET_OCTET_STRING *octet_string1, + size_t length, + const uint8_t *value); BACNET_STACK_EXPORT bool octetstring_value_same( const BACNET_OCTET_STRING *octet_string1, diff --git a/src/bacnet/basic/object/osv.c b/src/bacnet/basic/object/osv.c index c6649c74..b93cf834 100644 --- a/src/bacnet/basic/object/osv.c +++ b/src/bacnet/basic/object/osv.c @@ -21,6 +21,13 @@ #include "bacnet/basic/sys/keylist.h" #include "bacnet/basic/object/osv.h" +typedef struct octetstring_value_descr { + unsigned Event_State : 3; + bool Out_Of_Service; + BACNET_OCTET_STRING Present_Value; + const char *Object_Name; +} OCTETSTRING_VALUE_DESCR; + /* Key List for storing object data sorted by instance number */ static OS_Keylist Object_List = NULL; @@ -237,22 +244,77 @@ bool OctetString_Value_Present_Value_Set( return status; } +/** + * @brief Sets the present value length for an Octet String Value object. + * @param object_instance Object instance number. + * @param value Pointer to octet string value. + * @param length Length of the octet string value. + * @param priority Write priority (1..16). + * @return true if values are within range and present value length is set. + */ +bool OctetString_Value_Present_Value_Length_Set( + uint32_t object_instance, uint8_t *value, size_t length, uint8_t priority) +{ + OCTETSTRING_VALUE_DESCR *pObject = NULL; + bool status = false; + + (void)priority; + pObject = OctetString_Value_Object(object_instance); + if (pObject) { + status = octetstring_init(&pObject->Present_Value, value, length); + } + + return status; +} + /** * @brief Gets the present value for an Octet String Value object. * @param object_instance Object instance number. - * @return Pointer to present value, or NULL if object does not exist. + * @param value Pointer to octet string structure to receive the value. + * @return true if object exists and value is returned. */ -BACNET_OCTET_STRING *OctetString_Value_Present_Value(uint32_t object_instance) +bool OctetString_Value_Present_Value_Get( + uint32_t object_instance, BACNET_OCTET_STRING *value) { - BACNET_OCTET_STRING *value = NULL; OCTETSTRING_VALUE_DESCR *pObject = NULL; + bool status = false; pObject = OctetString_Value_Object(object_instance); if (pObject) { - value = &pObject->Present_Value; + status = octetstring_copy(value, &pObject->Present_Value); } - return value; + return status; +} + +/** + * @brief Gets the present value length for an Octet String Value object. + * @param object_instance Object instance number. + * @param value Pointer to buffer to receive octet string value. + * @param value_size Size of the value buffer. + * @param length Pointer to receive length of the octet string value. + * @return true if object exists and value length is returned. + */ +bool OctetString_Value_Present_Value_Length( + uint32_t object_instance, uint8_t *value, size_t value_size, size_t *length) +{ + OCTETSTRING_VALUE_DESCR *pObject = NULL; + size_t value_length = 0; + bool status = false; + + pObject = OctetString_Value_Object(object_instance); + if (pObject) { + value_length = octetstring_length(&pObject->Present_Value); + if (length) { + *length = value_length; + } + if (value && (value_size >= value_length)) { + memcpy(value, pObject->Present_Value.value, value_length); + } + status = true; + } + + return status; } /** @@ -323,6 +385,47 @@ const char *OctetString_Value_Name_ASCII(uint32_t object_instance) return name; } +/** + * For a given object instance-number, returns the out-of-service + * property value + * + * @param object_instance - object-instance number of the object + * + * @return out-of-service property value + */ +bool OctetString_Value_Out_Of_Service(uint32_t object_instance) +{ + bool value = false; + OCTETSTRING_VALUE_DESCR *pObject; + + pObject = OctetString_Value_Object(object_instance); + if (pObject) { + value = pObject->Out_Of_Service; + } + + return value; +} + +/** + * For a given object instance-number, sets the out-of-service property value + * + * @param object_instance - object-instance number of the object + * @param value - boolean out-of-service value + * + * @return true if the out-of-service property value was set + */ +bool OctetString_Value_Out_Of_Service_Set(uint32_t object_instance, bool value) +{ + OCTETSTRING_VALUE_DESCR *pObject; + + pObject = OctetString_Value_Object(object_instance); + if (pObject) { + pObject->Out_Of_Service = value; + return true; + } + return false; +} + /** * @brief Encodes a read-property response for an Octet String Value object. * @param rpdata Read property request/response context. @@ -331,27 +434,17 @@ const char *OctetString_Value_Name_ASCII(uint32_t object_instance) int OctetString_Value_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) { int apdu_len = 0; /* return value */ - BACNET_BIT_STRING bit_string; - BACNET_CHARACTER_STRING char_string; - BACNET_OCTET_STRING *real_value = NULL; + BACNET_BIT_STRING bit_string = { 0 }; + BACNET_CHARACTER_STRING char_string = { 0 }; + BACNET_OCTET_STRING octet_value = { 0 }; bool state = false; uint8_t *apdu = NULL; - OCTETSTRING_VALUE_DESCR *pObject = NULL; if ((rpdata == NULL) || (rpdata->application_data == NULL) || (rpdata->application_data_len == 0)) { return 0; } - apdu = rpdata->application_data; - - pObject = OctetString_Value_Object(rpdata->object_instance); - if (!pObject) { - rpdata->error_class = ERROR_CLASS_OBJECT; - rpdata->error_code = ERROR_CODE_UNKNOWN_OBJECT; - return BACNET_STATUS_ERROR; - } - switch (rpdata->object_property) { case PROP_OBJECT_IDENTIFIER: apdu_len = encode_application_object_id( @@ -372,9 +465,9 @@ int OctetString_Value_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) break; case PROP_PRESENT_VALUE: - real_value = - OctetString_Value_Present_Value(rpdata->object_instance); - apdu_len = encode_application_octet_string(&apdu[0], real_value); + OctetString_Value_Present_Value_Get( + rpdata->object_instance, &octet_value); + apdu_len = encode_application_octet_string(&apdu[0], &octet_value); break; case PROP_STATUS_FLAGS: @@ -384,7 +477,7 @@ int OctetString_Value_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) bitstring_set_bit(&bit_string, STATUS_FLAG_OVERRIDDEN, false); bitstring_set_bit( &bit_string, STATUS_FLAG_OUT_OF_SERVICE, - pObject->Out_Of_Service); + OctetString_Value_Out_Of_Service(rpdata->object_instance)); apdu_len = encode_application_bitstring(&apdu[0], &bit_string); break; @@ -395,7 +488,7 @@ int OctetString_Value_Read_Property(BACNET_READ_PROPERTY_DATA *rpdata) break; case PROP_OUT_OF_SERVICE: - state = pObject->Out_Of_Service; + state = OctetString_Value_Out_Of_Service(rpdata->object_instance); apdu_len = encode_application_boolean(&apdu[0], state); break; default: @@ -418,7 +511,6 @@ bool OctetString_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) bool status = false; /* return value */ int len = 0; BACNET_APPLICATION_DATA_VALUE value = { 0 }; - OCTETSTRING_VALUE_DESCR *pObject = NULL; if (wp_data == NULL) { return false; @@ -426,24 +518,15 @@ bool OctetString_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) if (wp_data->application_data_len == 0) { return false; } - /* decode the some of the request */ len = bacapp_decode_application_data( wp_data->application_data, wp_data->application_data_len, &value); - /* FIXME: len < application_data_len: more data? */ if (len < 0) { /* error while decoding - a value larger than we can handle */ wp_data->error_class = ERROR_CLASS_PROPERTY; wp_data->error_code = ERROR_CODE_VALUE_OUT_OF_RANGE; return false; } - pObject = OctetString_Value_Object(wp_data->object_instance); - if (!pObject) { - wp_data->error_class = ERROR_CLASS_OBJECT; - wp_data->error_code = ERROR_CODE_UNKNOWN_OBJECT; - return false; - } - switch (wp_data->object_property) { case PROP_PRESENT_VALUE: status = write_property_type_valid( @@ -473,7 +556,8 @@ bool OctetString_Value_Write_Property(BACNET_WRITE_PROPERTY_DATA *wp_data) status = write_property_type_valid( wp_data, &value, BACNET_APPLICATION_TAG_BOOLEAN); if (status) { - pObject->Out_Of_Service = value.type.Boolean; + OctetString_Value_Out_Of_Service_Set( + wp_data->object_instance, value.type.Boolean); } break; default: diff --git a/src/bacnet/basic/object/osv.h b/src/bacnet/basic/object/osv.h index 752baaeb..13b18f3f 100644 --- a/src/bacnet/basic/object/osv.h +++ b/src/bacnet/basic/object/osv.h @@ -20,13 +20,6 @@ extern "C" { #endif /* __cplusplus */ -typedef struct octetstring_value_descr { - unsigned Event_State : 3; - bool Out_Of_Service; - BACNET_OCTET_STRING Present_Value; - const char *Object_Name; -} OCTETSTRING_VALUE_DESCR; - BACNET_STACK_EXPORT void OctetString_Value_Property_Lists( const int32_t **pRequired, @@ -65,8 +58,22 @@ bool OctetString_Value_Present_Value_Set( const BACNET_OCTET_STRING *value, uint8_t priority); BACNET_STACK_EXPORT +bool OctetString_Value_Present_Value_Get( + uint32_t object_instance, BACNET_OCTET_STRING *value); +BACNET_STACK_DEPRECATED("Use OctetString_Value_Present_Value_Get() instead") +BACNET_STACK_EXPORT BACNET_OCTET_STRING *OctetString_Value_Present_Value(uint32_t object_instance); +BACNET_STACK_EXPORT +bool OctetString_Value_Present_Value_Length_Set( + uint32_t object_instance, uint8_t *value, size_t length, uint8_t priority); +BACNET_STACK_EXPORT +bool OctetString_Value_Present_Value_Length( + uint32_t object_instance, + uint8_t *value, + size_t value_size, + size_t *length); + BACNET_STACK_EXPORT bool OctetString_Value_Change_Of_Value(uint32_t instance); BACNET_STACK_EXPORT @@ -83,7 +90,7 @@ bool OctetString_Value_Description_Set(uint32_t instance, const char *new_name); BACNET_STACK_EXPORT bool OctetString_Value_Out_Of_Service(uint32_t instance); BACNET_STACK_EXPORT -void OctetString_Value_Out_Of_Service_Set(uint32_t instance, bool oos_flag); +bool OctetString_Value_Out_Of_Service_Set(uint32_t instance, bool oos_flag); BACNET_STACK_EXPORT uint32_t OctetString_Value_Create(uint32_t object_instance); diff --git a/test/bacnet/bacstr/src/main.c b/test/bacnet/bacstr/src/main.c index bf055a74..fe6bd7f4 100644 --- a/test/bacnet/bacstr/src/main.c +++ b/test/bacnet/bacstr/src/main.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -265,6 +266,232 @@ static void testCharacterString(void) zassert_false(status, NULL); } +/** + * @brief Test utf8_isvalid function + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacstr_tests, testUtf8IsValid) +#else +static void testUtf8IsValid(void) +#endif +{ + static const char ascii_value[] = "Joshua,Mary,Anna"; + static const char utf8_value[] = "Joshua😍Mary😍Anna"; + static const char valid_two_byte[] = { (char)0xC2, (char)0xA9 }; + static const char valid_three_byte[] = { (char)0xE2, (char)0x82, + (char)0xAC }; + static const char valid_five_byte[] = { (char)0xF8, (char)0x88, (char)0x80, + (char)0x80, (char)0x80 }; + static const char valid_six_byte[] = { (char)0xFC, (char)0x84, (char)0x80, + (char)0x80, (char)0x80, (char)0x80 }; + static const char embedded_nul[] = { 'A', '\0', 'B' }; + static const char lone_continuation[] = { (char)0x80 }; + static const char truncated_multibyte[] = { 'A', (char)0xF0 }; + static const char invalid_continuation[] = { (char)0xC2, 'A' }; + static const char invalid_late_continuation[] = { (char)0xE2, (char)0x82, + 'A' }; + static const char overlong_two_byte[] = { (char)0xC0, (char)0x80 }; + static const char overlong_three_byte[] = { (char)0xE0, (char)0x80, + (char)0x80 }; + static const char overlong_four_byte[] = { (char)0xF0, (char)0x80, + (char)0x80, (char)0x80 }; + static const char overlong_five_byte[] = { (char)0xF8, (char)0x80, + (char)0x80, (char)0x80, + (char)0x80 }; + static const char overlong_six_byte[] = { (char)0xFC, (char)0x80, + (char)0x80, (char)0x80, + (char)0x80, (char)0x80 }; + static const char invalid_fe[] = { (char)0xFE, (char)0x80, (char)0x80, + (char)0x80, (char)0x80, (char)0x80 }; + static const char invalid_ff[] = { (char)0xFF, (char)0x80, (char)0x80, + (char)0x80, (char)0x80, (char)0x80 }; + + zassert_true(utf8_isvalid(NULL, 0), "Empty input should be valid"); + zassert_true( + utf8_isvalid(ascii_value, strlen(ascii_value)), + "ASCII input should be valid UTF-8"); + zassert_true( + utf8_isvalid(valid_two_byte, sizeof(valid_two_byte)), + "Valid 2-byte UTF-8 should pass validation"); + zassert_true( + utf8_isvalid(valid_three_byte, sizeof(valid_three_byte)), + "Valid 3-byte UTF-8 should pass validation"); + zassert_true( + utf8_isvalid(utf8_value, strlen(utf8_value)), + "Valid multibyte UTF-8 should pass validation"); + zassert_true( + utf8_isvalid(valid_five_byte, sizeof(valid_five_byte)), + "Valid 5-byte legacy UTF-8 should pass validation"); + zassert_true( + utf8_isvalid(valid_six_byte, sizeof(valid_six_byte)), + "Valid 6-byte legacy UTF-8 should pass validation"); + + zassert_false(utf8_isvalid(NULL, 1), "NULL input should be rejected"); + zassert_false( + utf8_isvalid(embedded_nul, sizeof(embedded_nul)), + "Embedded NUL should be rejected"); + zassert_false( + utf8_isvalid(lone_continuation, sizeof(lone_continuation)), + "Lone continuation byte should be rejected"); + zassert_false( + utf8_isvalid(truncated_multibyte, sizeof(truncated_multibyte)), + "Truncated multibyte sequence should be rejected"); + zassert_false( + utf8_isvalid(invalid_continuation, sizeof(invalid_continuation)), + "Invalid continuation byte should be rejected"); + zassert_false( + utf8_isvalid( + invalid_late_continuation, sizeof(invalid_late_continuation)), + "Invalid later continuation byte should be rejected"); + zassert_false( + utf8_isvalid(overlong_two_byte, sizeof(overlong_two_byte)), + "Overlong 2-byte sequence should be rejected"); + zassert_false( + utf8_isvalid(overlong_three_byte, sizeof(overlong_three_byte)), + "Overlong 3-byte sequence should be rejected"); + zassert_false( + utf8_isvalid(overlong_four_byte, sizeof(overlong_four_byte)), + "Overlong 4-byte sequence should be rejected"); + zassert_false( + utf8_isvalid(overlong_five_byte, sizeof(overlong_five_byte)), + "Overlong 5-byte sequence should be rejected"); + zassert_false( + utf8_isvalid(overlong_six_byte, sizeof(overlong_six_byte)), + "Overlong 6-byte sequence should be rejected"); + zassert_false( + utf8_isvalid(invalid_fe, sizeof(invalid_fe)), + "0xFE lead byte should be rejected"); + zassert_false( + utf8_isvalid(invalid_ff, sizeof(invalid_ff)), + "0xFF lead byte should be rejected"); +} + +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacstr_tests, testCharacterStringUtf8Valid) +#else +static void testCharacterStringUtf8Valid(void) +#endif +{ + BACNET_CHARACTER_STRING bacnet_string = { 0 }; + const char *utf8_value = "Joshua😍Mary😍Anna"; + const char *ascii_value = "Joshua,Mary,Anna"; + bool status = false; + + /* Test NULL pointer */ + status = characterstring_utf8_valid(NULL); + zassert_false(status, "NULL pointer should return false"); + + /* Test non-UTF8 encoding - use CHARACTER_MS_DBCS (value 1) */ + status = characterstring_init_ansi(&bacnet_string, utf8_value); + zassert_true(status, NULL); + /* Verify it detects UTF-8 */ + zassert_equal( + characterstring_encoding(&bacnet_string), CHARACTER_UTF8, NULL); + /* Change encoding to a different non-UTF8 encoding (CHARACTER_MS_DBCS) */ + status = characterstring_set_encoding(&bacnet_string, CHARACTER_MS_DBCS); + zassert_true(status, NULL); + /* Now it should fail UTF-8 validation because encoding is not UTF-8 */ + status = characterstring_utf8_valid(&bacnet_string); + zassert_false(status, "Non-UTF8 encoding should return false"); + + /* Test valid UTF-8 string */ + status = characterstring_init_ansi(&bacnet_string, utf8_value); + zassert_true(status, NULL); + zassert_equal( + characterstring_encoding(&bacnet_string), CHARACTER_UTF8, NULL); + status = characterstring_utf8_valid(&bacnet_string); + zassert_true(status, "Valid UTF-8 string should return true"); + + /* Test empty UTF-8 string */ + status = characterstring_init(&bacnet_string, CHARACTER_UTF8, NULL, 0); + zassert_true(status, NULL); + status = characterstring_utf8_valid(&bacnet_string); + zassert_true(status, "Empty UTF-8 string should return true"); + + /* Test valid UTF-8 string with plain ASCII */ + status = characterstring_init( + &bacnet_string, CHARACTER_UTF8, ascii_value, strlen(ascii_value)); + zassert_true(status, NULL); + status = characterstring_utf8_valid(&bacnet_string); + zassert_true(status, "Valid ASCII-only UTF-8 string should return true"); +} + +/** + * @brief Test characterstring_utf8_strdup function + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacstr_tests, testCharacterStringUtf8Strdup) +#else +static void testCharacterStringUtf8Strdup(void) +#endif +{ + BACNET_CHARACTER_STRING bacnet_string = { 0 }; + const char *utf8_value = "Joshua😍Mary😍Anna"; + const char *ascii_value = "Joshua,Mary,Anna"; + char *dup_string = NULL; + bool status = false; + size_t length = 0; + size_t i = 0; + + /* Test NULL pointer */ + dup_string = characterstring_utf8_strdup(NULL); + zassert_is_null(dup_string, "NULL pointer should return NULL"); + + /* Test non-UTF8 encoding - use CHARACTER_MS_DBCS (value 1) */ + status = characterstring_init_ansi(&bacnet_string, utf8_value); + zassert_true(status, NULL); + /* Verify it detects UTF-8 */ + zassert_equal( + characterstring_encoding(&bacnet_string), CHARACTER_UTF8, NULL); + /* Change encoding to a different non-UTF8 encoding (CHARACTER_MS_DBCS) */ + status = characterstring_set_encoding(&bacnet_string, CHARACTER_MS_DBCS); + zassert_true(status, NULL); + dup_string = characterstring_utf8_strdup(&bacnet_string); + zassert_is_null(dup_string, "Non-UTF8 encoding should return NULL"); + + /* Test valid UTF-8 string duplication */ + status = characterstring_init_ansi(&bacnet_string, utf8_value); + zassert_true(status, NULL); + zassert_equal( + characterstring_encoding(&bacnet_string), CHARACTER_UTF8, NULL); + dup_string = characterstring_utf8_strdup(&bacnet_string); + zassert_not_null(dup_string, "Valid UTF-8 string should return non-NULL"); + length = characterstring_length(&bacnet_string); + /* Verify the duplicated string has correct content */ + for (i = 0; i < length; i++) { + zassert_equal( + dup_string[i], characterstring_value(&bacnet_string)[i], + "Duplicated strings should match at byte %u", i); + } + /* Verify NUL-termination */ + zassert_equal(dup_string[length], 0, "String should be NUL-terminated"); + free(dup_string); + dup_string = NULL; + + /* Test empty UTF-8 string duplication */ + status = characterstring_init(&bacnet_string, CHARACTER_UTF8, NULL, 0); + zassert_true(status, NULL); + dup_string = characterstring_utf8_strdup(&bacnet_string); + zassert_not_null(dup_string, "Empty UTF-8 string should return non-NULL"); + zassert_equal(dup_string[0], 0, "Empty string should be NUL-terminated"); + free(dup_string); + dup_string = NULL; + + /* Test valid UTF-8 string with plain ASCII */ + status = characterstring_init( + &bacnet_string, CHARACTER_UTF8, ascii_value, strlen(ascii_value)); + zassert_true(status, NULL); + dup_string = characterstring_utf8_strdup(&bacnet_string); + zassert_not_null(dup_string, "ASCII UTF-8 string should return non-NULL"); + length = characterstring_length(&bacnet_string); + zassert_equal( + strncmp(dup_string, ascii_value, length), 0, + "Duplicated ASCII-UTF8 string should match original"); + zassert_equal(dup_string[length], 0, "String should be NUL-terminated"); + free(dup_string); + dup_string = NULL; +} + /** * @brief Test encode/decode API for octet strings */ @@ -409,6 +636,87 @@ static void testOctetString(void) zassert_true(status, NULL); } +/** + * @brief Test octetstring_init_ascii_epics API + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacstr_tests, test_octetstring_init_ascii_epics) +#else +static void test_octetstring_init_ascii_epics(void) +#endif +{ + BACNET_OCTET_STRING bacnet_string; + const char *epics_valid_hex = "X'1234567890ABCDEF'"; + const char *epics_valid_hex_with_colons = "X'12:34:56:78:90:AB:CD:EF'"; + const char *epics_invalid_no_prefix = "1234567890ABCDEF"; + const char *epics_invalid_wrong_prefix = "H'1234567890ABCDEF'"; + const char *epics_invalid_odd_single = "X'1"; + char epics_too_long[MAX_APDU + MAX_APDU] = ""; + bool status = false; + size_t length = 0; + size_t test_length = 0; + uint8_t *value = NULL; + + /* test valid EPICS format with hex string */ + status = octetstring_init_ascii_epics(&bacnet_string, epics_valid_hex); + zassert_true(status, "Valid EPICS hex should return true"); + length = octetstring_length(&bacnet_string); + test_length = strlen(epics_valid_hex) - 3; /* subtract X'' */ + test_length = test_length / 2; /* convert hex chars to byte count */ + zassert_equal(length, test_length, "Length should be 8 bytes"); + value = octetstring_value(&bacnet_string); + zassert_equal(value[0], 0x12, "First byte should be 0x12"); + zassert_equal(value[1], 0x34, "Second byte should be 0x34"); + zassert_equal(value[7], 0xEF, "Last byte should be 0xEF"); + + /* test valid EPICS format with colons as separators */ + status = octetstring_init_ascii_epics( + &bacnet_string, epics_valid_hex_with_colons); + zassert_true(status, "Valid EPICS hex with colons should return true"); + length = octetstring_length(&bacnet_string); + zassert_equal(length, 8, "Length should be 8 bytes"); + value = octetstring_value(&bacnet_string); + zassert_equal(value[0], 0x12, "First byte should be 0x12"); + zassert_equal(value[1], 0x34, "Second byte should be 0x34"); + + /* test invalid - NULL octet string pointer */ + status = octetstring_init_ascii_epics(NULL, epics_valid_hex); + zassert_false(status, "NULL octet_string pointer should return false"); + + /* test invalid - NULL arg pointer */ + status = octetstring_init_ascii_epics(&bacnet_string, NULL); + zassert_false(status, "NULL arg pointer should return false"); + + /* test invalid - both NULL */ + status = octetstring_init_ascii_epics(NULL, NULL); + zassert_false(status, "Both NULL pointers should return false"); + + /* test invalid - no X' prefix */ + status = + octetstring_init_ascii_epics(&bacnet_string, epics_invalid_no_prefix); + zassert_false(status, "String without X' prefix should return false"); + + /* test invalid - wrong prefix */ + status = octetstring_init_ascii_epics( + &bacnet_string, epics_invalid_wrong_prefix); + zassert_false(status, "String with H' prefix should return false"); + + /* test invalid - odd number of hex digits */ + status = + octetstring_init_ascii_epics(&bacnet_string, epics_invalid_odd_single); + zassert_false(status, "Single hex digit without pair should return false"); + + /* test invalid - string too long */ + memset( + epics_too_long, 'F', + sizeof(epics_too_long) - 1); /* -1 for null terminator */ + epics_too_long[0] = 'X'; + epics_too_long[1] = '\''; + epics_too_long[sizeof(epics_too_long) - 1] = 0; /* null terminate */ + status = octetstring_init_ascii_epics(&bacnet_string, epics_too_long); + zassert_false(status, "String exceeding capacity should return false"); +} + /** * @brief Test encode/decode API for bacnet_stricmp */ @@ -828,6 +1136,73 @@ static void test_bacnet_snprintf(void) zassert_equal(null_len, test_null_len, "null_len=%d", null_len); } +/** + * @brief Test bacnet_strdup string duplication + */ +#if defined(CONFIG_ZTEST_NEW_API) +ZTEST(bacstr_tests, test_bacnet_strdup) +#else +static void test_bacnet_strdup(void) +#endif +{ + const char *original = "Test String"; + const char *empty_string = ""; + const char *long_string = + "This is a longer test string with multiple words"; + char *dup_string = NULL; + + /* Test NULL pointer */ + dup_string = bacnet_strdup(NULL); + zassert_is_null(dup_string, "NULL input should return NULL"); + + /* Test empty string */ + dup_string = bacnet_strdup(empty_string); + zassert_not_null(dup_string, "Empty string should allocate memory"); + zassert_equal( + strlen(dup_string), 0, "Duplicated empty string should have length 0"); + zassert_equal( + bacnet_strcmp(dup_string, empty_string), 0, + "Duplicated string should match original"); + free(dup_string); + + /* Test normal string */ + dup_string = bacnet_strdup(original); + zassert_not_null(dup_string, "Normal string should allocate memory"); + zassert_equal( + bacnet_strcmp(dup_string, original), 0, + "Duplicated string should match original"); + /* Verify different memory addresses */ + zassert_not_equal( + (uintptr_t)dup_string, (uintptr_t)original, + "Duplicated string should have different address"); + /* Verify string length is preserved */ + zassert_equal( + strlen(dup_string), strlen(original), + "String length should be preserved"); + free(dup_string); + + /* Test longer string */ + dup_string = bacnet_strdup(long_string); + zassert_not_null(dup_string, "Longer string should allocate memory"); + zassert_equal( + bacnet_strcmp(dup_string, long_string), 0, + "Duplicated longer string should match original"); + zassert_equal( + strlen(dup_string), strlen(long_string), + "Longer string length should be preserved"); + free(dup_string); + + /* Test string with special characters */ + const char *special_chars = "Hello\tWorld\nTest!"; + dup_string = bacnet_strdup(special_chars); + zassert_not_null( + dup_string, "String with special chars should allocate memory"); + zassert_equal( + bacnet_strcmp(dup_string, special_chars), 0, + "Special characters should be preserved"); + free(dup_string); +} + /** * @} */ @@ -839,7 +1214,11 @@ void test_main(void) { ztest_test_suite( bacstr_tests, ztest_unit_test(testBitString), - ztest_unit_test(testCharacterString), ztest_unit_test(testOctetString), + ztest_unit_test(testCharacterString), ztest_unit_test(testUtf8IsValid), + ztest_unit_test(testCharacterStringUtf8Valid), + ztest_unit_test(testCharacterStringUtf8Strdup), + ztest_unit_test(testOctetString), + ztest_unit_test(test_octetstring_init_ascii_epics), ztest_unit_test(test_bacnet_stricmp), ztest_unit_test(test_bacnet_strnicmp), ztest_unit_test(test_bacnet_strnlen), @@ -847,7 +1226,8 @@ void test_main(void) ztest_unit_test(test_bacnet_string_to_x), ztest_unit_test(test_bacnet_string_trim), ztest_unit_test(test_bacnet_stptok), - ztest_unit_test(test_bacnet_snprintf)); + ztest_unit_test(test_bacnet_snprintf), + ztest_unit_test(test_bacnet_strdup)); ztest_run_test_suite(bacstr_tests); } #endif