diff --git a/src/lib/support/BUILD.gn b/src/lib/support/BUILD.gn index 3323d8c9f33e67..618adc4e446e45 100644 --- a/src/lib/support/BUILD.gn +++ b/src/lib/support/BUILD.gn @@ -132,6 +132,7 @@ static_library("support") { "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 00000000000000..abeed6fa4567f9 --- /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 + +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(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 0c1aee8ed3efd9..a42ecec423a382 100644 --- a/src/lib/support/tests/BUILD.gn +++ b/src/lib/support/tests/BUILD.gn @@ -47,6 +47,7 @@ chip_test_suite("tests") { "TestSpan.cpp", "TestStateMachine.cpp", "TestStringBuilder.cpp", + "TestStringSplitter.cpp", "TestTestPersistentStorageDelegate.cpp", "TestThreadOperationalDataset.cpp", "TestTimeUtils.cpp", @@ -65,6 +66,9 @@ chip_test_suite("tests") { # 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 00000000000000..a8c41750211e1a --- /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 +#include + +#include + +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)