Skip to content

Commit

Permalink
Add a string splitter class (#27154)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Restyled.io <[email protected]>
  • Loading branch information
3 people authored and pull[bot] committed Aug 16, 2023
1 parent 35a02e4 commit 1223936
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/lib/support/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ static_library("support") {
"SetupDiscriminator.h",
"SortUtils.h",
"StateMachine.h",
"StringSplitter.h",
"ThreadOperationalDataset.cpp",
"ThreadOperationalDataset.h",
"TimeUtils.cpp",
Expand Down
88 changes: 88 additions & 0 deletions src/lib/support/StringSplitter.h
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions src/lib/support/tests/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ chip_test_suite("tests") {
"TestSpan.cpp",
"TestStateMachine.cpp",
"TestStringBuilder.cpp",
"TestStringSplitter.cpp",
"TestTestPersistentStorageDelegate.cpp",
"TestThreadOperationalDataset.cpp",
"TestTimeUtils.cpp",
Expand All @@ -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 = [
Expand Down
153 changes: 153 additions & 0 deletions src/lib/support/tests/TestStringSplitter.cpp
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 1223936

Please sign in to comment.