Aggregation: Check for subordinate collection

Adds a search function which is able to determine if a passed URI is a
top level collection, is uptree from a top level collection, or both.
The type being searched for depends on a second argument passed to the
function.

Each of these searches are used to add links to top level collections
which are only supported by a satellite BMC.  They all use similar steps
so rolling them into a single function cuts down on redundant code.

Adds test cases to verify the implementation is correct.

Tested:
New test cases pass

Signed-off-by: Carson Labrado <clabrado@google.com>
Change-Id: I72ae7442d5f314656b57a73aee544bca516fa7c2
diff --git a/redfish-core/include/redfish_aggregator.hpp b/redfish-core/include/redfish_aggregator.hpp
index c640586..245c698 100644
--- a/redfish-core/include/redfish_aggregator.hpp
+++ b/redfish-core/include/redfish_aggregator.hpp
@@ -21,6 +21,14 @@
     NoLocalHandle
 };
 
+enum class SearchType
+{
+    Collection,
+    CollOrCon,
+    ContainsSubordinate,
+    Resource
+};
+
 // clang-format off
 // These are all of the properties as of version 2022.2 of the Redfish Resource
 // and Schema Guide whose Type is "string (URI)" and the name does not end in a
@@ -43,6 +51,103 @@
 };
 // clang-format on
 
+// Search the top collection array to determine if the passed URI is of a
+// desired type
+inline bool searchCollectionsArray(std::string_view uri,
+                                   const SearchType searchType)
+{
+    constexpr std::string_view serviceRootUri = "/redfish/v1";
+
+    // The passed URI must begin with "/redfish/v1", but we have to strip it
+    // from the URI since topCollections does not include it in its URIs
+    if (!uri.starts_with(serviceRootUri))
+    {
+        return false;
+    }
+
+    // Catch empty final segments such as "/redfish/v1/Chassis//"
+    if (uri.ends_with("//"))
+    {
+        return false;
+    }
+
+    std::size_t parseCount = uri.size() - serviceRootUri.size();
+    // Don't include the trailing "/" if it exists such as in "/redfish/v1/"
+    if (uri.ends_with("/"))
+    {
+        parseCount--;
+    }
+
+    boost::urls::result<boost::urls::url_view> parsedUrl =
+        boost::urls::parse_relative_ref(
+            uri.substr(serviceRootUri.size(), parseCount));
+    if (!parsedUrl)
+    {
+        BMCWEB_LOG_ERROR << "Failed to get target URI from "
+                         << uri.substr(serviceRootUri.size());
+        return false;
+    }
+
+    if (!parsedUrl->segments().is_absolute() && !parsedUrl->segments().empty())
+    {
+        return false;
+    }
+
+    // If no segments() then the passed URI was either "/redfish/v1" or
+    // "/redfish/v1/".
+    if (parsedUrl->segments().empty())
+    {
+        return (searchType == SearchType::ContainsSubordinate) ||
+               (searchType == SearchType::CollOrCon);
+    }
+
+    const auto* it = std::lower_bound(
+        topCollections.begin(), topCollections.end(), parsedUrl->buffer());
+    if (it == topCollections.end())
+    {
+        // parsedUrl is alphabetically after the last entry in the array so it
+        // can't be a top collection or up tree from a top collection
+        return false;
+    }
+
+    boost::urls::url collectionUrl(*it);
+    boost::urls::segments_view collectionSegments = collectionUrl.segments();
+    boost::urls::segments_view::iterator itCollection =
+        collectionSegments.begin();
+    const boost::urls::segments_view::const_iterator endCollection =
+        collectionSegments.end();
+
+    // Each segment in the passed URI should match the found collection
+    for (const auto& segment : parsedUrl->segments())
+    {
+        if (itCollection == endCollection)
+        {
+            // Leftover segments means the target is for an aggregation
+            // supported resource
+            return searchType == SearchType::Resource;
+        }
+
+        if (segment != (*itCollection))
+        {
+            return false;
+        }
+        itCollection++;
+    }
+
+    // No remaining segments means the passed URI was a top level collection
+    if (searchType == SearchType::Collection)
+    {
+        return itCollection == endCollection;
+    }
+    if (searchType == SearchType::ContainsSubordinate)
+    {
+        return itCollection != endCollection;
+    }
+
+    // Return this check instead of "true" in case other SearchTypes get added
+    return searchType == SearchType::CollOrCon;
+}
+
 // Determines if the passed property contains a URI.  Those property names
 // either end with a case-insensitive version of "uri" or are specifically
 // defined in the above array.
diff --git a/test/redfish-core/include/redfish_aggregator_test.cpp b/test/redfish-core/include/redfish_aggregator_test.cpp
index 2fdd784..da3209a 100644
--- a/test/redfish-core/include/redfish_aggregator_test.cpp
+++ b/test/redfish-core/include/redfish_aggregator_test.cpp
@@ -518,5 +518,134 @@
     assertProcessResponseContentType(";charset=utf-8");
 }
 
+bool containsSubordinateCollection(const std::string_view uri)
+{
+    return searchCollectionsArray(uri, SearchType::ContainsSubordinate);
+}
+
+bool containsCollection(const std::string_view uri)
+{
+    return searchCollectionsArray(uri, SearchType::Collection);
+}
+
+bool isCollOrCon(const std::string_view uri)
+{
+    return searchCollectionsArray(uri, SearchType::CollOrCon);
+}
+
+TEST(searchCollectionsArray, containsSubordinateValidURIs)
+{
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/"));
+    EXPECT_TRUE(
+        containsSubordinateCollection("/redfish/v1/AggregationService"));
+    EXPECT_TRUE(
+        containsSubordinateCollection("/redfish/v1/CompositionService/"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/JobService"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/JobService/Log"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/KeyService"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/LicenseService/"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/PowerEquipment"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/TaskService"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/TelemetryService"));
+    EXPECT_TRUE(containsSubordinateCollection(
+        "/redfish/v1/TelemetryService/LogService/"));
+    EXPECT_TRUE(containsSubordinateCollection("/redfish/v1/UpdateService"));
+}
+
+TEST(searchCollectionsArray, containsSubordinateInvalidURIs)
+{
+    EXPECT_FALSE(containsSubordinateCollection(""));
+    EXPECT_FALSE(containsSubordinateCollection("http://"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish/"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish//"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish/v1//"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish/v11"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish/v11/"));
+    EXPECT_FALSE(containsSubordinateCollection("www.test.com/redfish/v1"));
+    EXPECT_FALSE(containsSubordinateCollection("/fail"));
+    EXPECT_FALSE(containsSubordinateCollection(
+        "/redfish/v1/AggregationService/Aggregates"));
+    EXPECT_FALSE(containsSubordinateCollection(
+        "/redfish/v1/AggregationService/AggregationSources/"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish/v1/Cables/"));
+    EXPECT_FALSE(
+        containsSubordinateCollection("/redfish/v1/Chassis/chassisId"));
+    EXPECT_FALSE(containsSubordinateCollection("/redfish/v1/Fake"));
+    EXPECT_FALSE(
+        containsSubordinateCollection("/redfish/v1/TelemetryService//"));
+    EXPECT_FALSE(containsSubordinateCollection(
+        "/redfish/v1/TelemetryService/LogService/Entries"));
+    EXPECT_FALSE(containsSubordinateCollection(
+        "/redfish/v1/UpdateService/SoftwareInventory/"));
+    EXPECT_FALSE(containsSubordinateCollection(
+        "/redfish/v1/UpdateService/SoftwareInventory/Te"));
+    EXPECT_FALSE(containsSubordinateCollection(
+        "/redfish/v1/UpdateService/SoftwareInventory2"));
+}
+
+TEST(searchCollectionsArray, collectionURIs)
+{
+    EXPECT_TRUE(containsCollection("/redfish/v1/Chassis"));
+    EXPECT_TRUE(containsCollection("/redfish/v1/Chassis/"));
+    EXPECT_TRUE(containsCollection("/redfish/v1/Managers"));
+    EXPECT_TRUE(containsCollection("/redfish/v1/Systems"));
+    EXPECT_TRUE(
+        containsCollection("/redfish/v1/TelemetryService/LogService/Entries"));
+    EXPECT_TRUE(
+        containsCollection("/redfish/v1/TelemetryService/LogService/Entries/"));
+    EXPECT_TRUE(
+        containsCollection("/redfish/v1/UpdateService/FirmwareInventory"));
+    EXPECT_TRUE(
+        containsCollection("/redfish/v1/UpdateService/FirmwareInventory/"));
+
+    EXPECT_FALSE(containsCollection("http://"));
+    EXPECT_FALSE(containsCollection("/redfish/v11/Chassis"));
+    EXPECT_FALSE(containsCollection("/redfish/v11/Chassis/"));
+    EXPECT_FALSE(containsCollection("/redfish/v1"));
+    EXPECT_FALSE(containsCollection("/redfish/v1/"));
+    EXPECT_FALSE(containsCollection("/redfish/v1//"));
+    EXPECT_FALSE(containsCollection("/redfish/v1/Chassis//"));
+    EXPECT_FALSE(containsCollection("/redfish/v1/Chassis/Test"));
+    EXPECT_FALSE(containsCollection("/redfish/v1/TelemetryService"));
+    EXPECT_FALSE(containsCollection("/redfish/v1/TelemetryService/"));
+    EXPECT_FALSE(containsCollection("/redfish/v1/UpdateService"));
+    EXPECT_FALSE(
+        containsCollection("/redfish/v1/UpdateService/FirmwareInventory/Test"));
+    EXPECT_FALSE(
+        containsCollection("/redfish/v1/UpdateService/FirmwareInventory/Tes/"));
+    EXPECT_FALSE(
+        containsCollection("/redfish/v1/UpdateService/SoftwareInventory/Te"));
+    EXPECT_FALSE(
+        containsCollection("/redfish/v1/UpdateService/SoftwareInventory2"));
+    EXPECT_FALSE(containsCollection("/redfish/v11"));
+    EXPECT_FALSE(containsCollection("/redfish/v11/"));
+}
+
+TEST(searchCollectionsArray, collectionOrContainsURIs)
+{
+    // Resources that are a top level collection or are uptree of one
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/AggregationService"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/CompositionService/"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/Chassis"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/Cables/"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/Fabrics"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/Managers"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/UpdateService/FirmwareInventory"));
+    EXPECT_TRUE(isCollOrCon("/redfish/v1/UpdateService/FirmwareInventory/"));
+
+    EXPECT_FALSE(isCollOrCon("http://"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v11"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v11/"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v1/Chassis/Test"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v1/Managers/Test/"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v1/TaskService/Tasks/0"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v1/UpdateService/FirmwareInventory/Te"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v1/UpdateService/SoftwareInventory/Te"));
+    EXPECT_FALSE(isCollOrCon("/redfish/v1/UpdateService/SoftwareInventory2"));
+}
+
 } // namespace
 } // namespace redfish