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