diff --git a/CHANGES.md b/CHANGES.md index e90734289..6ac7ba984 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,10 +4,12 @@ ##### Additions :tada: +- Added `Uri::getPath` and `Uri::setPath`. - Added `TileTransform::setTransform`. ##### Fixes :wrench: +- Fixed a bug in `joinToString` when given a collection containing empty strings. - `QuantizedMeshLoader` now creates spec-compliant glTFs from a quantized-mesh terrain tile. Previously, the generated glTF had small problems that could confuse some clients. ### v0.34.0 - 2024-04-01 diff --git a/CesiumUtility/include/CesiumUtility/Uri.h b/CesiumUtility/include/CesiumUtility/Uri.h index 63b95a941..aa21c63d9 100644 --- a/CesiumUtility/include/CesiumUtility/Uri.h +++ b/CesiumUtility/include/CesiumUtility/Uri.h @@ -24,5 +24,26 @@ class Uri final { const std::function& substitutionCallback); static std::string escape(const std::string& s); + + /** + * @brief Gets the path portion of the URI. This will not include path + * parameters, if present. + * + * @param uri The URI from which to get the path. + * @return The path, or empty string if the URI could not be parsed. + */ + static std::string getPath(const std::string& uri); + + /** + * @brief Sets the path portion of a URI to a new value. The other portions of + * the URI are left unmodified, including any path parameters. + * + * @param uri The URI for which to set the path. + * @param The new path portion of the URI. + * @returns The new URI after setting the path. If the original URI cannot be + * parsed, it is returned unmodified. + */ + static std::string + setPath(const std::string& uri, const std::string& newPath); }; } // namespace CesiumUtility diff --git a/CesiumUtility/include/CesiumUtility/joinToString.h b/CesiumUtility/include/CesiumUtility/joinToString.h index 848889913..d92a53fef 100644 --- a/CesiumUtility/include/CesiumUtility/joinToString.h +++ b/CesiumUtility/include/CesiumUtility/joinToString.h @@ -17,16 +17,17 @@ namespace CesiumUtility { template std::string joinToString(TIterator begin, TIterator end, const std::string& separator) { + if (begin == end) + return std::string(); + + std::string first = *begin; + return std::accumulate( - begin, + ++begin, end, - std::string(), + std::move(first), [&separator](const std::string& acc, const std::string& element) { - if (!acc.empty()) { - return acc + separator + element; - } else { - return element; - } + return acc + separator + element; }); } @@ -41,16 +42,6 @@ joinToString(TIterator begin, TIterator end, const std::string& separator) { */ template std::string joinToString(TCollection collection, const std::string& separator) { - return std::accumulate( - collection.cbegin(), - collection.cend(), - std::string(), - [&separator](const std::string& acc, const std::string& element) { - if (!acc.empty()) { - return acc + separator + element; - } else { - return element; - } - }); + return joinToString(collection.cbegin(), collection.cend(), separator); } } // namespace CesiumUtility diff --git a/CesiumUtility/src/Uri.cpp b/CesiumUtility/src/Uri.cpp index c7e3679d4..560d1700a 100644 --- a/CesiumUtility/src/Uri.cpp +++ b/CesiumUtility/src/Uri.cpp @@ -1,9 +1,13 @@ #include "CesiumUtility/Uri.h" +#include + #include +#include #include #include +#include namespace CesiumUtility { std::string Uri::resolve( @@ -175,4 +179,104 @@ std::string Uri::escape(const std::string& s) { result.resize(size_t(pTerminator - result.data())); return result; } + +std::string Uri::getPath(const std::string& uri) { + UriUriA parsedUri; + if (uriParseSingleUriA(&parsedUri, uri.c_str(), nullptr) != URI_SUCCESS) { + // Could not parse the URI, so return an empty string. + return std::string(); + } + + // The initial string in this vector can be thought of as the "nothing" before + // the first slash in the path. + std::vector parts{std::string()}; + + UriPathSegmentA* pCurrent = parsedUri.pathHead; + while (pCurrent != nullptr) { + parts.emplace_back(std::string( + pCurrent->text.first, + size_t(pCurrent->text.afterLast - pCurrent->text.first))); + pCurrent = pCurrent->next; + } + + uriFreeUriMembersA(&parsedUri); + + return joinToString(parts, "/"); +} + +std::string Uri::setPath(const std::string& uri, const std::string& newPath) { + UriUriA parsedUri; + if (uriParseSingleUriA(&parsedUri, uri.c_str(), nullptr) != URI_SUCCESS) { + // Could not parse the URI, so return an empty string. + return std::string(); + } + + // Free the existing path. Strangely, uriparser doesn't provide any simple way + // to do this. + UriPathSegmentA* pCurrent = parsedUri.pathHead; + while (pCurrent != nullptr) { + UriPathSegmentA* pNext = pCurrent->next; + free(pCurrent); + pCurrent = pNext; + } + + parsedUri.pathHead = nullptr; + parsedUri.pathTail = nullptr; + + // Set the new path. + if (!newPath.empty()) { + std::string::size_type startPos = 0; + do { + std::string::size_type nextSlashIndex = newPath.find('/', startPos); + + // Skip the initial slash if there is one. + if (nextSlashIndex == 0) { + startPos = 1; + continue; + } + + UriPathSegmentA* pSegment = + static_cast(malloc(sizeof(UriPathSegmentA))); + memset(pSegment, 0, sizeof(UriPathSegmentA)); + + if (parsedUri.pathHead == nullptr) { + parsedUri.pathHead = pSegment; + parsedUri.pathTail = pSegment; + } else { + parsedUri.pathTail->next = pSegment; + parsedUri.pathTail = parsedUri.pathTail->next; + } + + pSegment->text.first = newPath.data() + startPos; + + if (nextSlashIndex != std::string::npos) { + pSegment->text.afterLast = newPath.data() + nextSlashIndex; + startPos = nextSlashIndex + 1; + } else { + pSegment->text.afterLast = newPath.data() + newPath.size(); + startPos = nextSlashIndex; + } + } while (startPos != std::string::npos); + } + + int charsRequired; + if (uriToStringCharsRequiredA(&parsedUri, &charsRequired) != URI_SUCCESS) { + uriFreeUriMembersA(&parsedUri); + return uri; + } + + std::string result(static_cast(charsRequired), ' '); + + if (uriToStringA( + const_cast(result.c_str()), + &parsedUri, + charsRequired + 1, + nullptr) != URI_SUCCESS) { + uriFreeUriMembersA(&parsedUri); + return uri; + } + + return result; +} + } // namespace CesiumUtility diff --git a/CesiumUtility/test/TestJoinToString.cpp b/CesiumUtility/test/TestJoinToString.cpp new file mode 100644 index 000000000..7edbba008 --- /dev/null +++ b/CesiumUtility/test/TestJoinToString.cpp @@ -0,0 +1,34 @@ +#include + +#include + +using namespace CesiumUtility; + +TEST_CASE("joinToString") { + SECTION("joins vector with non-empty elements") { + CHECK( + joinToString(std::vector{"test", "this"}, "--") == + "test--this"); + CHECK( + joinToString(std::vector{"test", "this", "thing"}, " ") == + "test this thing"); + CHECK( + joinToString(std::vector{"test", "this", "thing"}, "") == + "testthisthing"); + } + + SECTION("joins vector with empty elements") { + CHECK( + joinToString(std::vector{"", "aa", ""}, "--") == "--aa--"); + CHECK(joinToString(std::vector{"", ""}, "--") == "--"); + } + + SECTION("handles single-element vector") { + CHECK(joinToString(std::vector{"test"}, "--") == "test"); + CHECK(joinToString(std::vector{""}, "--") == ""); + } + + SECTION("handles empty vector") { + CHECK(joinToString(std::vector{}, "--") == ""); + } +} diff --git a/CesiumUtility/test/TestUri.cpp b/CesiumUtility/test/TestUri.cpp new file mode 100644 index 000000000..c3cde6844 --- /dev/null +++ b/CesiumUtility/test/TestUri.cpp @@ -0,0 +1,83 @@ +#include + +#include + +using namespace CesiumUtility; + +TEST_CASE("Uri::getPath") { + SECTION("returns path") { + CHECK(Uri::getPath("https://example.com/") == "/"); + CHECK(Uri::getPath("https://example.com/foo/bar") == "/foo/bar"); + CHECK(Uri::getPath("https://example.com/foo/bar/") == "/foo/bar/"); + } + + SECTION("ignores path parameters") { + CHECK(Uri::getPath("https://example.com/?some=parameter") == "/"); + CHECK( + Uri::getPath("https://example.com/foo/bar?some=parameter") == + "/foo/bar"); + CHECK( + Uri::getPath("https://example.com/foo/bar/?some=parameter") == + "/foo/bar/"); + } + + SECTION("returns empty path for nonexistent paths") { + CHECK(Uri::getPath("https://example.com") == ""); + CHECK(Uri::getPath("https://example.com?some=parameter") == ""); + } + + SECTION("returns empty path for invalid uri") { + CHECK(Uri::getPath("not a valid uri") == ""); + } +} + +TEST_CASE("Uri::setPath") { + SECTION("sets empty path") { + CHECK(Uri::setPath("https://example.com", "") == "https://example.com"); + } + + SECTION("sets new path") { + CHECK(Uri::setPath("https://example.com", "/") == "https://example.com/"); + CHECK( + Uri::setPath("https://example.com/foo", "/bar") == + "https://example.com/bar"); + CHECK( + Uri::setPath("https://example.com/foo/", "/bar") == + "https://example.com/bar"); + CHECK( + Uri::setPath("https://example.com/foo", "/bar/") == + "https://example.com/bar/"); + } + + SECTION("preserves path parameters") { + CHECK( + Uri::setPath("https://example.com?some=parameter", "") == + "https://example.com?some=parameter"); + CHECK( + Uri::setPath("https://example.com?some=parameter", "/") == + "https://example.com/?some=parameter"); + CHECK( + Uri::setPath("https://example.com/foo?some=parameter", "/bar") == + "https://example.com/bar?some=parameter"); + CHECK( + Uri::setPath("https://example.com/foo/?some=parameter", "/bar") == + "https://example.com/bar?some=parameter"); + CHECK( + Uri::setPath("https://example.com/foo?some=parameter", "/bar/") == + "https://example.com/bar/?some=parameter"); + } + + SECTION("sets same path") { + CHECK( + Uri::setPath("https://example.com/foo/bar", "/foo/bar") == + "https://example.com/foo/bar"); + CHECK( + Uri::setPath( + "https://example.com/foo/bar?some=parameter", + "/foo/bar") == "https://example.com/foo/bar?some=parameter"); + } + + SECTION("returns empty path for invalid uri") { + CHECK(Uri::setPath("not a valid uri", "/foo/") == ""); + } +}