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
This commit is contained in:
Steve Karg
2026-03-16 17:04:23 -05:00
committed by GitHub
parent 07cf52384b
commit a755a8db4d
6 changed files with 563 additions and 59 deletions
+382 -2
View File
@@ -9,6 +9,7 @@
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <stdint.h>
#include <zephyr/ztest.h>
#include <bacnet/bacstr.h>
@@ -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