Implement alternative to on boost::split

boost::split has a documented false-positive in clang-tidy.  While
normally we'd handle this with NOLINTNEXTLINE, this doesn't appear to
work in all cases.  Unclear why, but seems to be due to some of our
lambda callback complexity.

Each of these uses is a case where we should be using a more specific
check, rather than split, but for the moment, this is the best we have.

Tested: clang-tidy passes.

[1] https://github.com/llvm/llvm-project/issues/40486

Signed-off-by: Ed Tanous <edtanous@google.com>
Change-Id: I144c6610cb740287b7225e2be03b4142a64f9563
diff --git a/include/openbmc_dbus_rest.hpp b/include/openbmc_dbus_rest.hpp
index 8dd7bb4..2665b2e 100644
--- a/include/openbmc_dbus_rest.hpp
+++ b/include/openbmc_dbus_rest.hpp
@@ -21,6 +21,7 @@
 #include "http_response.hpp"
 #include "logging.hpp"
 #include "routing.hpp"
+#include "str_utility.hpp"
 
 #include <systemd/sd-bus-protocol.h>
 #include <systemd/sd-bus.h>
@@ -28,7 +29,6 @@
 
 #include <boost/algorithm/string/classification.hpp>
 #include <boost/algorithm/string/predicate.hpp>
-#include <boost/algorithm/string/split.hpp>
 #include <boost/beast/http/status.hpp>
 #include <boost/beast/http/verb.hpp>
 #include <boost/container/flat_map.hpp>
@@ -2089,7 +2089,8 @@
                         const std::string& requestedPath)
 {
     std::vector<std::string> strs;
-    boost::split(strs, requestedPath, boost::is_any_of("/"));
+
+    bmcweb::split(strs, requestedPath, '/');
     std::string objectPath;
     std::string interfaceName;
     std::string methodName;
diff --git a/include/str_utility.hpp b/include/str_utility.hpp
new file mode 100644
index 0000000..39e1c82
--- /dev/null
+++ b/include/str_utility.hpp
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace bmcweb
+{
+// This is a naive replacement for boost::split until
+// https://github.com/llvm/llvm-project/issues/40486
+// is resolved
+inline void split(std::vector<std::string>& strings, std::string_view str,
+                  char delim)
+{
+    size_t start = 0;
+    size_t end = 0;
+    while ((start = str.find_first_not_of(delim, end)) != std::string::npos)
+    {
+        end = str.find(delim, start);
+        strings.emplace_back(str.substr(start, end - start));
+    }
+}
+} // namespace bmcweb
diff --git a/meson.build b/meson.build
index 79dfe5a..be3c6b1 100644
--- a/meson.build
+++ b/meson.build
@@ -366,6 +366,7 @@
   'test/include/ibm/lock_test.cpp',
   'test/include/multipart_test.cpp',
   'test/include/openbmc_dbus_rest_test.cpp',
+  'test/include/str_utility_test.cpp',
   'test/redfish-core/include/privileges_test.cpp',
   'test/redfish-core/include/redfish_aggregator_test.cpp',
   'test/redfish-core/include/registries_test.cpp',
diff --git a/redfish-core/include/event_service_manager.hpp b/redfish-core/include/event_service_manager.hpp
index 7da9100..976d043 100644
--- a/redfish-core/include/event_service_manager.hpp
+++ b/redfish-core/include/event_service_manager.hpp
@@ -26,13 +26,13 @@
 #include "registries/openbmc_message_registry.hpp"
 #include "registries/task_event_message_registry.hpp"
 #include "server_sent_events.hpp"
+#include "str_utility.hpp"
 #include "utility.hpp"
 #include "utils/json_utils.hpp"
 
 #include <sys/inotify.h>
 
 #include <boost/algorithm/string/classification.hpp>
-#include <boost/algorithm/string/split.hpp>
 #include <boost/asio/io_context.hpp>
 #include <boost/container/flat_map.hpp>
 #include <sdbusplus/bus/match.hpp>
@@ -121,7 +121,8 @@
     // the right Message
     std::vector<std::string> fields;
     fields.reserve(4);
-    boost::split(fields, messageID, boost::is_any_of("."));
+
+    bmcweb::split(fields, messageID, '.');
     if (fields.size() != 4)
     {
         return nullptr;
@@ -189,8 +190,7 @@
     entry.remove_prefix(entryStart);
     // Use split to separate the entry into its fields
     std::vector<std::string> logEntryFields;
-    boost::split(logEntryFields, entry, boost::is_any_of(","),
-                 boost::token_compress_on);
+    bmcweb::split(logEntryFields, entry, ',');
     // We need at least a MessageId to be valid
     if (logEntryFields.empty())
     {
@@ -222,7 +222,7 @@
     // the right Message
     std::vector<std::string> fields;
     fields.reserve(4);
-    boost::split(fields, messageID, boost::is_any_of("."));
+    bmcweb::split(fields, messageID, '.');
     if (fields.size() == 4)
     {
         registryName = fields[0];
@@ -301,8 +301,9 @@
                     sseFilter.end());
 
     std::vector<std::string> result;
-    boost::split(result, sseFilter, boost::is_any_of(" "),
-                 boost::token_compress_on);
+
+    // NOLINTNEXTLINE
+    bmcweb::split(result, sseFilter, ' ');
 
     BMCWEB_LOG_DEBUG << "No of tokens in SEE query: " << result.size();
 
diff --git a/redfish-core/include/utils/query_param.hpp b/redfish-core/include/utils/query_param.hpp
index bfe1001..9c38248 100644
--- a/redfish-core/include/utils/query_param.hpp
+++ b/redfish-core/include/utils/query_param.hpp
@@ -7,11 +7,11 @@
 #include "http_request.hpp"
 #include "http_response.hpp"
 #include "logging.hpp"
+#include "str_utility.hpp"
 
 #include <sys/types.h>
 
 #include <boost/algorithm/string/classification.hpp>
-#include <boost/algorithm/string/split.hpp>
 #include <boost/beast/http/message.hpp> // IWYU pragma: keep
 #include <boost/beast/http/status.hpp>
 #include <boost/beast/http/verb.hpp>
@@ -351,7 +351,7 @@
 inline bool getSelectParam(std::string_view value, Query& query)
 {
     std::vector<std::string> properties;
-    boost::split(properties, value, boost::is_any_of(","));
+    bmcweb::split(properties, value, ',');
     if (properties.empty())
     {
         return false;
diff --git a/redfish-core/lib/log_services.hpp b/redfish-core/lib/log_services.hpp
index ef1652b..89c38b0 100644
--- a/redfish-core/lib/log_services.hpp
+++ b/redfish-core/lib/log_services.hpp
@@ -98,7 +98,7 @@
     // the right Message
     std::vector<std::string> fields;
     fields.reserve(4);
-    boost::split(fields, messageID, boost::is_any_of("."));
+    bmcweb::split(fields, messageID, '.');
     const std::string& registryName = fields[0];
     const std::string& messageKey = fields[3];
 
@@ -1238,8 +1238,7 @@
     entry.remove_prefix(entryStart);
     // Use split to separate the entry into its fields
     std::vector<std::string> logEntryFields;
-    boost::split(logEntryFields, entry, boost::is_any_of(","),
-                 boost::token_compress_on);
+    bmcweb::split(logEntryFields, entry, ',');
     // We need at least a MessageId to be valid
     if (logEntryFields.empty())
     {
@@ -3616,7 +3615,7 @@
                                  uint64_t& currentValue, uint16_t& index)
 {
     std::vector<std::string> split;
-    boost::algorithm::split(split, postCodeID, boost::is_any_of("-"));
+    bmcweb::split(split, postCodeID, '-');
     if (split.size() != 2 || split[0].length() < 2 || split[0].front() != 'B')
     {
         return false;
diff --git a/redfish-core/lib/sensors.hpp b/redfish-core/lib/sensors.hpp
index 6be9e86..e6d7a7a 100644
--- a/redfish-core/lib/sensors.hpp
+++ b/redfish-core/lib/sensors.hpp
@@ -21,6 +21,7 @@
 #include "generated/enums/sensor.hpp"
 #include "query.hpp"
 #include "registries/privilege_registry.hpp"
+#include "str_utility.hpp"
 #include "utils/dbus_utils.hpp"
 #include "utils/json_utils.hpp"
 #include "utils/query_param.hpp"
@@ -29,7 +30,6 @@
 #include <boost/algorithm/string/find.hpp>
 #include <boost/algorithm/string/predicate.hpp>
 #include <boost/algorithm/string/replace.hpp>
-#include <boost/algorithm/string/split.hpp>
 #include <boost/range/algorithm/replace_copy_if.hpp>
 #include <boost/system/error_code.hpp>
 #include <sdbusplus/asio/property.hpp>
@@ -2281,14 +2281,15 @@
                 // Reserve space for
                 // /xyz/openbmc_project/sensors/<name>/<subname>
                 split.reserve(6);
-                boost::algorithm::split(split, objPath, boost::is_any_of("/"));
+                // NOLINTNEXTLINE
+                bmcweb::split(split, objPath, '/');
                 if (split.size() < 6)
                 {
                     BMCWEB_LOG_ERROR << "Got path that isn't long enough "
                                      << objPath;
                     continue;
                 }
-                // These indexes aren't intuitive, as boost::split puts an empty
+                // These indexes aren't intuitive, as split puts an empty
                 // string at the beginning
                 const std::string& sensorType = split[4];
                 const std::string& sensorName = split[5];
diff --git a/test/include/str_utility_test.cpp b/test/include/str_utility_test.cpp
new file mode 100644
index 0000000..bab14c9
--- /dev/null
+++ b/test/include/str_utility_test.cpp
@@ -0,0 +1,31 @@
+#include "str_utility.hpp"
+
+#include <string>
+#include <vector>
+
+#include <gmock/gmock.h> // IWYU pragma: keep
+#include <gtest/gtest.h> // IWYU pragma: keep
+
+// IWYU pragma: no_include <gtest/gtest-message.h>
+// IWYU pragma: no_include <gtest/gtest-test-part.h>
+// IWYU pragma: no_include "gtest/gtest_pred_impl.h"
+// IWYU pragma: no_include <gmock/gmock-matchers.h>
+
+namespace
+{
+
+using ::testing::ElementsAre;
+using ::testing::IsEmpty;
+
+TEST(Split, PositiveTests)
+{
+    using bmcweb::split;
+    std::vector<std::string> vec;
+    split(vec, "xx-abc-xx-abb", '-');
+    EXPECT_THAT(vec, ElementsAre("xx", "abc", "xx", "abb"));
+    vec.clear();
+    split(vec, "", '-');
+    EXPECT_THAT(vec, IsEmpty());
+}
+
+} // namespace