From 448bcba1dc0825d60fc10f838e02687943cd9464 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Tue, 21 Jul 2026 16:14:49 +0100 Subject: [PATCH] Fix region command whitespace parsing --- src/Utils.cpp | 18 ++++++++++++- src/Utils.h | 10 ++++++++ src/helpers/CommonCLI.cpp | 2 +- test/test_utils/test_tohex.cpp | 47 ++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/Utils.cpp b/src/Utils.cpp index 186c8720a2..1263ce5f44 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -150,4 +150,20 @@ int Utils::parseTextParts(char* text, const char* parts[], int max_num, char sep return num; } -} \ No newline at end of file +int Utils::parseTextPartsSkippingEmpty(char* text, const char* parts[], int max_num, char separator) { + int num = 0; + char* sp = text; + while (*sp && num < max_num) { + while (*sp == separator) sp++; + if (*sp == '\0') break; + + parts[num++] = sp; + while (*sp && *sp != separator) sp++; + if (*sp) { + *sp++ = 0; + } + } + return num; +} + +} diff --git a/src/Utils.h b/src/Utils.h index 5736b8747a..549269b9a4 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -81,6 +81,16 @@ class Utils { */ static int parseTextParts(char* text, const char* parts[], int max_num, char separator=','); + /** + * \brief parse 'text' into non-empty parts separated by 'separator' char. + * \param text the text to parse (note is MODIFIED!) + * \param parts destination array to store pointers to starts of parse parts + * \param max_num max elements to store in 'parts' array + * \param separator the separator character; consecutive instances are treated as one + * \returns the number of non-empty parts parsed (in 'parts') + */ + static int parseTextPartsSkippingEmpty(char* text, const char* parts[], int max_num, char separator=','); + static bool isHexChar(char c); }; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b78ad6ebd6..7faee572a9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1002,7 +1002,7 @@ void CommonCLI::handleRegionCmd(char* command, char* reply) { } const char* parts[4]; - int n = mesh::Utils::parseTextParts(command, parts, 4, ' '); + int n = mesh::Utils::parseTextPartsSkippingEmpty(command, parts, 4, ' '); if (n == 1) { _region_map->exportTo(reply, 160); } else if (n >= 2 && strcmp(parts[1], "load") == 0) { diff --git a/test/test_utils/test_tohex.cpp b/test/test_utils/test_tohex.cpp index fec3ae4877..37c1e2353d 100644 --- a/test/test_utils/test_tohex.cpp +++ b/test/test_utils/test_tohex.cpp @@ -51,6 +51,53 @@ TEST(UtilsToHex, NullTerminatesOnEmptyInput) { EXPECT_EQ('\0', output[0]); } +TEST(UtilsParseTextParts, PreservesEmptyPartsByDefault) { + char input[] = "region default "; + const char* parts[4]; + + int count = Utils::parseTextParts(input, parts, 4, ' '); + + ASSERT_EQ(3, count); + EXPECT_STREQ("region", parts[0]); + EXPECT_STREQ("default", parts[1]); + EXPECT_STREQ("", parts[2]); +} + +TEST(UtilsParseTextParts, SkippingEmptyTreatsTrailingSpacesAsNoArgument) { + char input[] = "region default "; + const char* parts[4]; + + int count = Utils::parseTextPartsSkippingEmpty(input, parts, 4, ' '); + + ASSERT_EQ(2, count); + EXPECT_STREQ("region", parts[0]); + EXPECT_STREQ("default", parts[1]); +} + +TEST(UtilsParseTextParts, SkippingEmptyKeepsNamedArgumentBetweenRepeatedSpaces) { + char input[] = "region default au-nsw "; + const char* parts[4]; + + int count = Utils::parseTextPartsSkippingEmpty(input, parts, 4, ' '); + + ASSERT_EQ(3, count); + EXPECT_STREQ("region", parts[0]); + EXPECT_STREQ("default", parts[1]); + EXPECT_STREQ("au-nsw", parts[2]); +} + +TEST(UtilsParseTextParts, KeepsExplicitNullRegionArgument) { + char input[] = "region default "; + const char* parts[4]; + + int count = Utils::parseTextPartsSkippingEmpty(input, parts, 4, ' '); + + ASSERT_EQ(3, count); + EXPECT_STREQ("region", parts[0]); + EXPECT_STREQ("default", parts[1]); + EXPECT_STREQ("", parts[2]); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();