Add a string splitter class (#27154)
* Add a string splitter class
* Do not fail on stringop-truncation in tests
* Fix initialization of mData for null cases for stringsplitter
* Restyled by clang-format
* Add more unit tests and ensure final empty element is emitted
* Restyled by clang-format
* separator should be const
* Review comment: use char span
* Restyled by clang-format
* Force a static cast for size compares
---------
Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/lib/support/BUILD.gn b/src/lib/support/BUILD.gn
index 3323d8c..618adc4 100644
--- a/src/lib/support/BUILD.gn
+++ b/src/lib/support/BUILD.gn
@@ -132,6 +132,7 @@
"SetupDiscriminator.h",
"SortUtils.h",
"StateMachine.h",
+ "StringSplitter.h",
"ThreadOperationalDataset.cpp",
"ThreadOperationalDataset.h",
"TimeUtils.cpp",
diff --git a/src/lib/support/StringSplitter.h b/src/lib/support/StringSplitter.h
new file mode 100644
index 0000000..abeed6f
--- /dev/null
+++ b/src/lib/support/StringSplitter.h
@@ -0,0 +1,88 @@
+/*
+ *
+ * Copyright (c) 2023 Project CHIP Authors
+ * All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <lib/support/Span.h>
+
+namespace chip {
+
+/// Provides the ability to split a given string by a character.
+///
+/// Converts things like:
+/// "a,b,c" split by ',': "a", "b", "c"
+/// ",b,c" split by ',': "", "b", "c"
+/// "a,,c" split by ',': "a", "", "c"
+/// "a," split by ',': "a", ""
+/// ",a" split by ',': "", "a"
+///
+///
+/// WARNING: WILL DESTRUCTIVELY MODIFY THE STRING IN PLACE
+///
+class StringSplitter
+{
+public:
+ StringSplitter(const char * s, char separator) : mNext(s), mSeparator(separator)
+ {
+ if ((mNext != nullptr) && (*mNext == '\0'))
+ {
+ mNext = nullptr; // end of string right away
+ }
+ }
+
+ /// Returns the next character san
+ ///
+ /// out - contains the next element or a nullptr/0 sized span if
+ /// no elements available
+ ///
+ /// Returns true if an element is available, false otherwise.
+ bool Next(CharSpan & out)
+ {
+ if (mNext == nullptr)
+ {
+ out = CharSpan();
+ return false; // nothing left
+ }
+
+ const char * end = mNext;
+ while ((*end != '\0') && (*end != mSeparator))
+ {
+ end++;
+ }
+
+ if (*end != '\0')
+ {
+ // intermediate element
+ out = CharSpan(mNext, static_cast<size_t>(end - mNext));
+ mNext = end + 1;
+ }
+ else
+ {
+ // last element
+ out = CharSpan::fromCharString(mNext);
+ mNext = nullptr;
+ }
+
+ return true;
+ }
+
+protected:
+ const char * mNext; // start of next element to return by Next()
+ const char mSeparator;
+};
+
+} // namespace chip
diff --git a/src/lib/support/tests/BUILD.gn b/src/lib/support/tests/BUILD.gn
index 0c1aee8..a42ecec 100644
--- a/src/lib/support/tests/BUILD.gn
+++ b/src/lib/support/tests/BUILD.gn
@@ -47,6 +47,7 @@
"TestSpan.cpp",
"TestStateMachine.cpp",
"TestStringBuilder.cpp",
+ "TestStringSplitter.cpp",
"TestTestPersistentStorageDelegate.cpp",
"TestThreadOperationalDataset.cpp",
"TestTimeUtils.cpp",
@@ -65,6 +66,9 @@
# TODO(#21255): work-around for SimpleStateMachine constructor issue.
"-Wno-uninitialized",
+
+ # TestStringSplitter intentionally validates string overflows.
+ "-Wno-stringop-truncation",
]
public_deps = [
diff --git a/src/lib/support/tests/TestStringSplitter.cpp b/src/lib/support/tests/TestStringSplitter.cpp
new file mode 100644
index 0000000..a8c4175
--- /dev/null
+++ b/src/lib/support/tests/TestStringSplitter.cpp
@@ -0,0 +1,153 @@
+/*
+ *
+ * Copyright (c) 2023 Project CHIP Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <lib/support/StringSplitter.h>
+#include <lib/support/UnitTestRegistration.h>
+
+#include <nlunit-test.h>
+
+namespace {
+
+using namespace chip;
+
+void TestStrdupSplitter(nlTestSuite * inSuite, void * inContext)
+{
+ CharSpan out;
+
+ // empty string handling
+ {
+ StringSplitter splitter("", ',');
+
+ // next stays at nullptr
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data() == nullptr);
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data() == nullptr);
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data() == nullptr);
+ }
+
+ // single item
+ {
+ StringSplitter splitter("single", ',');
+
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("single")));
+
+ // next stays at nullptr also after valid data
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data() == nullptr);
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data() == nullptr);
+ }
+
+ // multi-item
+ {
+ StringSplitter splitter("one,two,three", ',');
+
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("one")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("two")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("three")));
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data() == nullptr);
+ }
+
+ // mixed
+ {
+ StringSplitter splitter("a**bc*d,e*f", '*');
+
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("a")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("bc")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("d,e")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("f")));
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ }
+
+ // some edge cases
+ {
+ StringSplitter splitter(",", ',');
+ // Note that even though "" is nullptr right away, "," becomes two empty strings
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ }
+ {
+ StringSplitter splitter("log,", ',');
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("log")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ }
+ {
+ StringSplitter splitter(",log", ',');
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("log")));
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ }
+ {
+ StringSplitter splitter(",,,", ',');
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, splitter.Next(out));
+ NL_TEST_ASSERT(inSuite, out.data_equal(CharSpan::fromCharString("")));
+ NL_TEST_ASSERT(inSuite, !splitter.Next(out));
+ }
+}
+
+void TestNullResilience(nlTestSuite * inSuite, void * inContext)
+{
+ {
+ StringSplitter splitter(nullptr, ',');
+ CharSpan span;
+ NL_TEST_ASSERT(inSuite, !splitter.Next(span));
+ NL_TEST_ASSERT(inSuite, span.data() == nullptr);
+ }
+}
+
+const nlTest sTests[] = {
+ NL_TEST_DEF("TestSplitter", TestStrdupSplitter), //
+ NL_TEST_DEF("TestNullResilience", TestNullResilience), //
+ NL_TEST_SENTINEL() //
+};
+
+} // namespace
+
+int TestStringSplitter()
+{
+ nlTestSuite theSuite = { "StringSplitter", sTests, nullptr, nullptr };
+ nlTestRunner(&theSuite, nullptr);
+ return nlTestRunnerStats(&theSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestStringSplitter)