Merge branch '3dtiles-voxel-content' into tilers-lilleyse

This commit is contained in:
Sean Lilley 2025-02-05 12:19:14 -05:00
commit 80362072cb
23 changed files with 682 additions and 471 deletions

View File

@ -1,5 +1,22 @@
# Change Log
### ? - ?
##### Breaking Changes :mega:
- Removed `TilesetOptions::maximumSimultaneousSubtreeLoads` because it was unused.
### Additions :tada:
- Added `convertPropertyComponentTypeToAccessorComponentType` to `PropertyType`.
- Added support for `3DTILES_ellipsoid` in `Cesium3DTiles`, `Cesium3DTilesReader`, and `Cesium3DTilesWriter`.
### v0.44.1 - 2025-02-03
##### Fixes :wrench:
- Fixed a bug in `CesiumIonClient::Connection` that caused the `authorize` method to use an incorrect URL.
### v0.44.0 - 2025-02-03
##### Breaking Changes :mega:
@ -18,8 +35,6 @@
- Added `UrlTemplateRasterOverlay` for requesting raster tiles from services using a templated URL.
- `upsampleGltfForRasterOverlays` is now compatible with meshes using TRIANGLE_STRIP, TRIANGLE_FAN, or non-indexed TRIANGLES primitives.
- Added `requestHeaders` field to `TilesetOptions` to allow per-tileset request headers to be specified.
- Added support for `3DTILES_ellipsoid` in `CesiumGltf`, `CesiumGltfReader`, and `CesiumGltfWriter`.
- Added `convertPropertyComponentTypeToAccessorComponentType` to `PropertyType`.
##### Fixes :wrench:
@ -27,7 +42,9 @@
- Fixed a bug in `SharedAssetDepot` that could cause assertion failures in debug builds, and could rarely cause premature deletion of shared assets even in release builds.
- Fixed a bug that could cause `Tileset::sampleHeightMostDetailed` to return a height that is not the highest one when the sampled tileset contained multiple heights at the given location.
- `LayerJsonTerrainLoader` will now log errors and warnings when failing to load a `.terrain` file referenced in the layer.json, instead of silently ignoring them.
- URIs containing unicode characters are now supported.
- Fixed a crash in `CullingVolume` when the camera was very far away from the globe.
- Fixed a bug that prevented the `culture` parameter of the `BingMapsRasterOverlay` from having an effect.
### v0.43.0 - 2025-01-02

View File

@ -46,8 +46,8 @@ message(STATUS "VCPKG_OVERLAY_TRIPLETS ${VCPKG_OVERLAY_TRIPLETS}")
# These packages are used in the public headers of Cesium libraries, so we need to distribute the headers and binaries
# with the installation
# Note that fmt is a public dependency of the vcpkg version of spdlog
# STB and uriparser aren't technically part of the public interface, but they're used by the downstream Cesium for Unreal project
set(PACKAGES_PUBLIC asyncplusplus expected-lite fmt glm rapidjson spdlog stb uriparser)
# STB is not technically part of the public interface, but it is used by the downstream Cesium for Unreal project
set(PACKAGES_PUBLIC asyncplusplus expected-lite fmt glm rapidjson spdlog stb ada-url)
# These packages are used in the code and produce binaries, but are not part of the public interface. Therefore we need
# to distribute the binaries for linking, but not the headers, as downstream consumers don't need them
# OpenSSL and abseil are both dependencies of s2geometry
@ -87,7 +87,7 @@ endif()
include("cmake/defaults.cmake")
project(cesium-native
VERSION 0.43.0
VERSION 0.44.0
LANGUAGES CXX C
)
@ -257,8 +257,8 @@ find_package(s2 CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
find_package(tinyxml2 CONFIG REQUIRED)
find_package(unofficial-sqlite3 CONFIG REQUIRED)
find_package(uriparser CONFIG REQUIRED char wchar_t)
find_package(WebP CONFIG REQUIRED)
find_package(ada CONFIG REQUIRED)
# Private Library (s2geometry)

View File

@ -2,6 +2,7 @@
#include <doctest/doctest.h>
#include <algorithm>
#include <cstring>
using namespace Cesium3DTiles;

View File

@ -54,7 +54,6 @@ target_link_libraries(Cesium3DTilesSelection
CesiumUtility
spdlog::spdlog spdlog::spdlog_header_only
# PRIVATE
uriparser::uriparser
libmorton::libmorton
draco::draco
nonstd::expected-lite

View File

@ -102,12 +102,6 @@ struct CESIUM3DTILESSELECTION_API TilesetOptions {
*/
uint32_t maximumSimultaneousTileLoads = 20;
/**
* @brief The maximum number of subtrees that may simultaneously be in the
* process of loading.
*/
uint32_t maximumSimultaneousSubtreeLoads = 20;
/**
* @brief Indicates whether the ancestors of rendered tiles should be
* preloaded. Setting this to true optimizes the zoom-out experience and

View File

@ -340,13 +340,6 @@ Future<LoadLayersResult> loadLayersRecursive(
std::string extensionsToRequest =
createExtensionsQueryParameter(knownExtensions, extensions);
if (!extensionsToRequest.empty()) {
for (std::string& url : urls) {
url =
CesiumUtility::Uri::addQuery(url, "extensions", extensionsToRequest);
}
}
const auto availabilityLevelsIt =
layerJson.FindMember("metadataAvailability");
@ -379,6 +372,7 @@ Future<LoadLayersResult> loadLayersRecursive(
baseUrl,
std::move(version),
std::move(urls),
std::move(extensionsToRequest),
std::move(availability),
static_cast<uint32_t>(maxZoom),
availabilityLevels);
@ -648,12 +642,14 @@ LayerJsonTerrainLoader::Layer::Layer(
const std::string& baseUrl_,
std::string&& version_,
std::vector<std::string>&& tileTemplateUrls_,
std::string&& extensionsToRequest_,
CesiumGeometry::QuadtreeRectangleAvailability&& contentAvailability_,
uint32_t maxZooms_,
int32_t availabilityLevels_)
: baseUrl{baseUrl_},
version{std::move(version_)},
tileTemplateUrls{std::move(tileTemplateUrls_)},
extensionsToRequest{std::move(extensionsToRequest_)},
contentAvailability{std::move(contentAvailability_)},
loadedSubtrees(maxSubtreeInLayer(maxZooms_, availabilityLevels_)),
availabilityLevels{availabilityLevels_} {}
@ -675,9 +671,9 @@ std::string resolveTileUrl(
return std::string();
}
return CesiumUtility::Uri::resolve(
Uri uri(
layer.baseUrl,
CesiumUtility::Uri::substituteTemplateParameters(
Uri::substituteTemplateParameters(
layer.tileTemplateUrls[0],
[&tileID, &layer](const std::string& placeholder) -> std::string {
if (placeholder == "level" || placeholder == "z") {
@ -695,6 +691,14 @@ std::string resolveTileUrl(
return placeholder;
}));
if (!layer.extensionsToRequest.empty()) {
UriQuery params(uri);
params.setValue("extensions", layer.extensionsToRequest);
uri.setQuery(params.toQueryString());
}
return std::string(uri.toString());
}
Future<QuantizedMeshLoadResult> requestTileContent(

View File

@ -52,6 +52,7 @@ public:
const std::string& baseUrl,
std::string&& version,
std::vector<std::string>&& tileTemplateUrls,
std::string&& extensionsToRequest,
CesiumGeometry::QuadtreeRectangleAvailability&& contentAvailability,
uint32_t maxZooms,
int32_t availabilityLevels);
@ -59,6 +60,7 @@ public:
std::string baseUrl;
std::string version;
std::vector<std::string> tileTemplateUrls;
std::string extensionsToRequest;
CesiumGeometry::QuadtreeRectangleAvailability contentAvailability;
std::vector<std::unordered_set<uint64_t>> loadedSubtrees;
int32_t availabilityLevels;

View File

@ -270,7 +270,8 @@ TEST_CASE("Test create layer json terrain loader") {
CHECK(layers[0].version == "1.33.0");
CHECK(
layers[0].tileTemplateUrls.front() ==
"{z}/{x}/{y}.terrain?v={version}&extensions=octvertexnormals-metadata");
"{z}/{x}/{y}.terrain?v={version}");
CHECK(layers[0].extensionsToRequest == "octvertexnormals-metadata");
CHECK(layers[0].loadedSubtrees.size() == 2);
CHECK(layers[0].availabilityLevels == 10);
}
@ -299,7 +300,8 @@ TEST_CASE("Test create layer json terrain loader") {
CHECK(layers[0].version == "1.0.0");
CHECK(
layers[0].tileTemplateUrls.front() ==
"{z}/{x}/{y}.terrain?v={version}&extensions=octvertexnormals");
"{z}/{x}/{y}.terrain?v={version}");
CHECK(layers[0].extensionsToRequest == "octvertexnormals");
CHECK(layers[0].loadedSubtrees.empty());
CHECK(layers[0].availabilityLevels == -1);
@ -322,7 +324,7 @@ TEST_CASE("Test create layer json terrain loader") {
auto parentJsonPath =
testDataPath / "CesiumTerrainTileJson" / "Parent.tile.json";
pMockedAssetAccessor->mockCompletedRequests.insert(
{"./Parent/layer.json", createMockAssetRequest(parentJsonPath)});
{"Parent/layer.json", createMockAssetRequest(parentJsonPath)});
auto loaderFuture =
LayerJsonTerrainLoader::createLoader(externals, {}, "layer.json", {});
@ -418,10 +420,8 @@ TEST_CASE("Test create layer json terrain loader") {
const auto& layers = loaderResult.pLoader->getLayers();
CHECK(layers.size() == 1);
CHECK(layers[0].tileTemplateUrls.size() == 1);
CHECK(
layers[0].tileTemplateUrls[0] ==
"{z}/{x}/"
"{y}.terrain?v={version}&extensions=octvertexnormals-watermask");
CHECK(layers[0].tileTemplateUrls[0] == "{z}/{x}/{y}.terrain?v={version}");
CHECK(layers[0].extensionsToRequest == "octvertexnormals-watermask");
}
}
@ -452,6 +452,7 @@ TEST_CASE("Test load layer json tile content") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}.terrain"},
"one-two",
std::move(contentAvailability),
maxZoom,
10);
@ -460,7 +461,7 @@ TEST_CASE("Test load layer json tile content") {
// mock tile content request
pMockedAssetAccessor->mockCompletedRequests.insert(
{"0.0.0/1.0.0.terrain",
{"0.0.0/1.0.0.terrain?extensions=one-two",
createMockAssetRequest(
testDataPath / "CesiumTerrainTileJson" /
"tile.metadataavailability.terrain")});
@ -502,6 +503,7 @@ TEST_CASE("Test load layer json tile content") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}.terrain"},
std::string(),
std::move(contentAvailability),
maxZoom,
-1);
@ -563,6 +565,7 @@ TEST_CASE("Test load layer json tile content") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}_layer0.terrain"},
std::string(),
std::move(layer0ContentAvailability),
maxZoom,
-1);
@ -577,6 +580,7 @@ TEST_CASE("Test load layer json tile content") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}_layer1.terrain"},
std::string(),
std::move(layer1ContentAvailability),
maxZoom,
-1);
@ -639,6 +643,7 @@ TEST_CASE("Test load layer json tile content") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}_layer0.terrain"},
std::string(),
std::move(layer0ContentAvailability),
maxZoom,
10);
@ -650,6 +655,7 @@ TEST_CASE("Test load layer json tile content") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}_layer1.terrain"},
std::string(),
std::move(layer1ContentAvailability),
maxZoom,
10);
@ -724,6 +730,7 @@ TEST_CASE("Test load layer json tile content") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}.terrain"},
std::string{},
std::move(contentAvailability),
maxZoom,
10);
@ -784,6 +791,7 @@ TEST_CASE("Test creating tile children for layer json") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}_layer0.terrain"},
std::string(),
std::move(layer0ContentAvailability),
maxZoom,
10);
@ -799,6 +807,7 @@ TEST_CASE("Test creating tile children for layer json") {
"layer.json",
"1.0.0",
std::vector<std::string>{"{level}.{x}.{y}/{version}_layer1.terrain"},
std::string(),
std::move(layer1ContentAvailability),
maxZoom,
10);

View File

@ -17,7 +17,7 @@
"geometricError": 0,
"refine": "ADD",
"content": {
"uri": "tileset3/ll.b3dm"
"uri": "ll.b3dm"
}
}
}

View File

@ -29,8 +29,6 @@
#include <rapidjson/rapidjson.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <uriparser/Uri.h>
#include <uriparser/UriBase.h>
#include <algorithm>
#include <cstddef>
@ -173,7 +171,7 @@ std::string createAuthorizationErrorHtml(
authorizeUrl =
Uri::addQuery(authorizeUrl, "client_id", std::to_string(clientID));
authorizeUrl =
Uri::addQuery(authorizeUrl, "scope", joinToString(scopes, "%20"));
Uri::addQuery(authorizeUrl, "scope", joinToString(scopes, " "));
authorizeUrl = Uri::addQuery(authorizeUrl, "redirect_uri", redirectUrl);
authorizeUrl = Uri::addQuery(authorizeUrl, "state", state);
authorizeUrl = Uri::addQuery(authorizeUrl, "code_challenge_method", "S256");
@ -495,19 +493,18 @@ Asset jsonToAsset(const rapidjson::Value& item) {
}
std::optional<std::string> generateApiUrl(const std::string& ionUrl) {
UriUriA newUri;
if (uriParseSingleUriA(&newUri, ionUrl.c_str(), nullptr) != URI_SUCCESS) {
return std::optional<std::string>();
Uri parsedIonUrl(ionUrl);
if (!parsedIonUrl) {
return std::nullopt;
}
std::string hostName =
std::string(newUri.hostText.first, newUri.hostText.afterLast);
std::string scheme =
std::string(newUri.scheme.first, newUri.scheme.afterLast);
std::string url;
url.append(parsedIonUrl.getScheme());
url.append("//api.");
url.append(parsedIonUrl.getHost());
url.append("/");
uriFreeUriMembersA(&newUri);
return std::make_optional<std::string>(scheme + "://api." + hostName + '/');
return url;
}
} // namespace

View File

@ -132,49 +132,46 @@ public:
width,
height),
_credits(perTileCredits),
_baseUrl(baseUrl),
_urlTemplate(urlTemplate),
_subdomains(subdomains) {
if (this->_urlTemplate.find("n=z") == std::string::npos) {
this->_urlTemplate =
CesiumUtility::Uri::addQuery(this->_urlTemplate, "n", "z");
}
std::string resolvedUrl =
CesiumUtility::Uri::resolve(baseUrl, this->_urlTemplate);
resolvedUrl = CesiumUtility::Uri::substituteTemplateParameters(
resolvedUrl,
[&culture](const std::string& templateKey) {
if (templateKey == "culture") {
return culture;
}
// Keep other placeholders
return "{" + templateKey + "}";
});
}
_culture(culture),
_subdomains(subdomains) {}
virtual ~BingMapsTileProvider() = default;
protected:
virtual CesiumAsync::Future<LoadedRasterOverlayImage> loadQuadtreeTileImage(
const CesiumGeometry::QuadtreeTileID& tileID) const override {
std::string url = CesiumUtility::Uri::substituteTemplateParameters(
this->_urlTemplate,
[this, &tileID](const std::string& key) {
if (key == "quadkey") {
return BingMapsTileProvider::tileXYToQuadKey(
tileID.level,
tileID.x,
tileID.computeInvertedY(this->getTilingScheme()));
}
if (key == "subdomain") {
const size_t subdomainIndex =
(tileID.level + tileID.x + tileID.y) % this->_subdomains.size();
return this->_subdomains[subdomainIndex];
}
return key;
});
Uri uri(
this->_baseUrl,
CesiumUtility::Uri::substituteTemplateParameters(
this->_urlTemplate,
[this, &tileID](const std::string& key) {
if (key == "quadkey") {
return BingMapsTileProvider::tileXYToQuadKey(
tileID.level,
tileID.x,
tileID.computeInvertedY(this->getTilingScheme()));
}
if (key == "subdomain") {
const size_t subdomainIndex =
(tileID.level + tileID.x + tileID.y) %
this->_subdomains.size();
return this->_subdomains[subdomainIndex];
}
if (key == "culture") {
return this->_culture;
}
return key;
}));
UriQuery query(uri);
if (!query.hasValue("n")) {
query.setValue("n", "z");
uri.setQuery(query.toQueryString());
}
std::string url = std::string(uri.toString());
LoadTileImageFromUrlOptions options;
options.allowEmptyImages = true;
@ -233,7 +230,9 @@ private:
}
std::vector<CreditAndCoverageAreas> _credits;
std::string _baseUrl;
std::string _urlTemplate;
std::string _culture;
std::vector<std::string> _subdomains;
};
@ -357,14 +356,21 @@ BingMapsRasterOverlay::createTileProvider(
pPrepareRendererResources,
const std::shared_ptr<spdlog::logger>& pLogger,
IntrusivePointer<const RasterOverlay> pOwner) const {
std::string metadataUrl = CesiumUtility::Uri::resolve(
Uri metadataUri(
this->_url,
"REST/v1/Imagery/Metadata/" + this->_mapStyle,
true);
metadataUrl =
CesiumUtility::Uri::addQuery(metadataUrl, "incl", "ImageryProviders");
metadataUrl = CesiumUtility::Uri::addQuery(metadataUrl, "key", this->_key);
metadataUrl = CesiumUtility::Uri::addQuery(metadataUrl, "uriScheme", "https");
UriQuery metadataQuery(metadataUri);
metadataQuery.setValue("incl", "ImageryProviders");
metadataQuery.setValue("key", this->_key);
metadataQuery.setValue("uriScheme", "https");
if (!this->_culture.empty()) {
metadataQuery.setValue("culture", this->_culture);
}
metadataUri.setQuery(metadataQuery.toQueryString());
std::string metadataUrl = std::string(metadataUri.toString());
pOwner = pOwner ? pOwner : this;

View File

@ -46,5 +46,5 @@ target_link_libraries(
zlib-ng::zlib-ng
spdlog::spdlog
glm::glm
uriparser::uriparser
ada::ada
)

View File

@ -1,15 +1,112 @@
#pragma once
#include <ada.h>
#include <ada/url_search_params.h>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
namespace CesiumUtility {
/**
* @brief A class for building and manipulating Uniform Resource Identifiers
* @brief A class for parsing and manipulating Uniform Resource Identifiers
* (URIs).
*
* The URI parser supports the [WhatWG URL
* specification](https://url.spec.whatwg.org/). It also supports
* protocol-relative URIs such as `//example.com`, and opaque paths such as
* `a/b/c`.
*/
class Uri final {
public:
/**
* @brief Attempts to create a new Uri by parsing the given string. If the
* string fails to parse, \ref isValid will return false.
*
* @param uri A string containing the URI to attempt to parse.
*/
Uri(const std::string& uri);
/**
* @brief Attempts to create a new Uri from a base URI and a relative URI. If
* the base URI is invalid, only the relative URI string will be used. If the
* relative URI fails to parse, \ref isValid will return false.
*
* @param base The base URI that the relative URI is relative to.
* @param relative A string containing a relative URI to attempt to parse.
* @param useBaseQuery If true, the resulting URI will include the query
* parameters of both the base URI and the relative URI. If false, only the
* relative URI's query parameters will be used (if any).
*/
Uri(const Uri& base, const std::string& relative, bool useBaseQuery = false);
/**
* @brief Returns a string representation of the entire URI including path and
* query parameters.
*/
std::string_view toString() const;
/**
* @brief Returns true if this URI has been successfully parsed.
*/
bool isValid() const;
/**
* @brief Equivalent to \ref isValid.
*/
operator bool() const { return this->isValid(); }
/**
* @brief Gets the scheme portion of the URI. If the URI was created without a
* scheme, this will return an empty string.
*
* @returns The scheme, or an empty string if the URI could not be parsed or
* has no scheme.
*/
std::string_view getScheme() const;
/**
* @brief Gets the host portion of the URI. If the URI also specifies a
* non-default port, it will be included in the returned host. If the URI
* contains no host, this will return an empty string.
*
* @returns The host, or an empty string if the URI could not be parsed or has
* no host.
*/
std::string_view getHost() const;
/**
* @brief Gets the path portion of the URI. This will not include query
* parameters, if present.
*
* @return The path, or empty string if the URI could not be parsed.
*/
std::string_view getPath() const;
/**
* @brief Gets the query portion of the URI.
*
* @return The path, or empty string if the URI could not be parsed.
*/
std::string_view getQuery() const;
/**
* @brief Sets the path portion of a URI to a new value. The other portions of
* the URI are left unmodified, including any query parameters.
*
* @param path The new path portion of the URI.
*/
void setPath(const std::string_view& path);
/**
* @brief Sets the query portion of a URI to a new value. The other portions
* of the URI are left unmodified.
*
* @param queryString The new query portion of the URI.
*/
void setQuery(const std::string_view& queryString);
/**
* @brief Attempts to resolve a relative URI using a base URI.
*
@ -21,10 +118,11 @@ public:
* @param relative The relative URI to be resolved against the base URI.
* @param useBaseQuery If true, any query parameters of the base URI will be
* retained in the resolved URI.
* @param assumeHttpsDefault If true, protocol-relative URIs (such as
* `//api.cesium.com`) will be assumed to be `https`. If false, they will be
* assumed to be `http`.
* @param assumeHttpsDefault This parameter is ignored and is only kept for
* API compatibility.
* @returns The resolved URI.
*
* @deprecated Use the \ref Uri constructor instead.
*/
static std::string resolve(
const std::string& base,
@ -40,6 +138,14 @@ public:
* @param key The key to be added to the query string.
* @param value The value to be added to the query string.
* @returns The modified URI including the new query string parameter.
*
* @deprecated Use the \ref UriQuery class:
* ```
* Uri parsedUri(uri);
* UriQuery params(parsedUri);
* params.setValue(key, value);
* parsedUri.setQuery(params.toQueryString());
* ```
*/
static std::string addQuery(
const std::string& uri,
@ -56,6 +162,13 @@ public:
* @param key The key whose value will be obtained from the URI, if possible.
* @returns The value of the given key in the query string, or an empty string
* if not found.
*
* @deprecated Use the \ref UriQuery class:
* ```
* Uri parsedUri(uri);
* UriQuery params(parsedUri);
* params.getValue(key);
* ```
*/
static std::string
getQueryValue(const std::string& uri, const std::string& key);
@ -193,6 +306,8 @@ public:
*
* @param uri The URI from which to get the path.
* @return The path, or empty string if the URI could not be parsed.
*
* @deprecated Create a \ref Uri instance and use \ref Uri::getPath() instead.
*/
static std::string getPath(const std::string& uri);
@ -204,8 +319,97 @@ public:
* @param newPath 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.
*
* @deprecated Create a \ref Uri instance and use \ref Uri::setPath(const
* std::string_view&) instead.
*/
static std::string
setPath(const std::string& uri, const std::string& newPath);
private:
std::optional<ada::url_aggregator> _url = std::nullopt;
bool _hasScheme = false;
};
/**
* @brief A class for parsing and manipulating the query string of a URI.
*/
class UriQuery final {
public:
/**
* @brief Creates a \ref UriQuery object from a query string.
*
* This query string should be in the format
* `key1=value1&key2=value2&key3=value3...`. This is the format returned by
* \ref Uri::getQuery. This string can include percent-encoded values.
*
* @param queryString The query string to parse into a query params object.
*/
UriQuery(const std::string_view& queryString) : _params(queryString) {}
/**
* @brief Creates a \ref UriQuery object from a \ref Uri instance.
*
* This is equivalent to `UriQuery(uri.getQuery())`.
*
* @param uri The URI instance to obtain the query params from.
*/
UriQuery(const Uri& uri) : _params(uri.getQuery()) {}
/**
* @brief Obtains the value of the given key from the query parameters,
* if possible.
*
* If the URI can't be parsed, or the key doesn't exist in the
* query string, `std::nullopt` will be returned.
*
* @param key The key whose value will be obtained from the query string, if
* possible.
* @returns The value of the given key in the query string, or `std::nullopt`
* if not found.
*/
std::optional<std::string_view> getValue(const std::string& key) {
return this->_params.get(key);
}
/**
* @brief Sets the given key in the query parameters to the given value.
* If the key doesn't exist already, it will be added to the query parameters.
* Otherwise, the previous value will be overwritten.
*
* @param key The key to be added to the query string.
* @param value The value to be added to the query string.
*/
void setValue(const std::string& key, const std::string& value) {
this->_params.set(key, value);
}
/**
* @brief Returns true if this query string contains a value for the given
* key, or false otherwise.
*
* @param key The key to check.
*/
bool hasValue(const std::string& key) { return this->_params.has(key); }
/**
* @brief Converts this object back into a query string, including all
* modifications that have been made. This result can be passed directly to
* \ref Uri::setQuery.
*/
std::string toQueryString() const { return this->_params.to_string(); }
/** @brief Returns an iterator pointing to the beginning of the query
* parameters. */
inline auto begin() const { return this->_params.begin(); }
/** @brief Returns an iterator pointing to the end of the query parameters. */
inline auto end() const { return this->_params.end(); }
/** @brief Returns the first element in the query parameters. */
inline auto front() const { return this->_params.front(); }
/** @brief Returns the last element in the query parameters. */
inline auto back() const { return this->_params.back(); }
private:
ada::url_search_params _params;
};
} // namespace CesiumUtility

View File

@ -1,167 +1,188 @@
#include <CesiumUtility/Uri.h>
#include <CesiumUtility/joinToString.h>
#include <uriparser/Uri.h>
#include <uriparser/UriBase.h>
#include <ada/character_sets-inl.h>
#include <ada/implementation.h>
#include <ada/unicode.h>
#include <ada/url_aggregator.h>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <stdexcept>
#include <optional>
#include <string>
#include <vector>
#include <string_view>
#include <utility>
namespace CesiumUtility {
namespace {
const char* const HTTPS_PREFIX = "https:";
const char* const HTTP_PREFIX = "http:";
const std::string HTTPS_PREFIX = "https:";
const std::string FILE_PREFIX = "file:///";
const char WINDOWS_PATH_SEP = '\\';
const char PATH_SEP = '/';
std::string cesiumConformUrl(const std::string& url, bool useHttps) {
// Prepend protocol to protocol-relative URIs.
if (url.length() > 2 && url.at(0) == '/' && url.at(1) == '/') {
return std::string(useHttps ? HTTPS_PREFIX : HTTP_PREFIX).append(url);
using UrlResult = ada::result<ada::url_aggregator>;
// C++ locale settings might change which values std::isalpha checks for. We
// only want ASCII.
bool isAsciiAlpha(unsigned char c) {
return c >= 0x41 && c <= 0x7a && (c <= 0x5a || c >= 0x61);
}
bool isAscii(unsigned char c) { return c <= 0x7f; }
/**
* A URI has a valid scheme if it starts with an ASCII alpha character and has a
* sequence of ASCII characters followed by a "://"
*/
bool urlHasScheme(const std::string& uri) {
for (size_t i = 0; i < uri.length(); i++) {
unsigned char c = static_cast<unsigned char>(uri[i]);
if (c == ':') {
return uri.length() > i + 2 && uri[i + 1] == '/' && uri[i + 2] == '/';
} else if ((i == 0 && !isAsciiAlpha(c)) || !isAscii(c)) {
// Scheme must start with an ASCII alpha character and be an ASCII string
return false;
}
}
return url;
return false;
}
} // namespace
// clang-tidy isn't smart enough to understand that we *are* checking the
// optionals before accessing them, so disable the warning.
// NOLINTBEGIN(bugprone-unchecked-optional-access)
Uri::Uri(const std::string& uri) {
UrlResult result;
if (uri.starts_with("//")) {
// This is a protocol-relative URL.
// We will treat it as an HTTPS URL.
this->_hasScheme = true;
result = ada::parse(HTTPS_PREFIX + uri);
} else {
this->_hasScheme = urlHasScheme(uri);
result = this->_hasScheme ? ada::parse(uri) : ada::parse(FILE_PREFIX + uri);
}
if (result) {
this->_url.emplace(std::move(result.value()));
}
}
Uri::Uri(const Uri& base, const std::string& relative, bool useBaseQuery) {
UrlResult result;
if (!base.isValid()) {
this->_hasScheme = urlHasScheme(relative);
result = this->_hasScheme ? ada::parse(relative)
: ada::parse(FILE_PREFIX + relative);
} else {
this->_hasScheme = base._hasScheme;
result = ada::parse(relative, &base._url.value());
}
if (result) {
this->_url.emplace(std::move(result.value()));
if (useBaseQuery) {
UriQuery baseParams(base);
UriQuery relativeParams(*this);
// Set from relative to base to give priority to relative URL query string
for (const auto& [key, value] : baseParams) {
if (!relativeParams.hasValue(key)) {
relativeParams.setValue(key, value);
}
}
this->_url->set_search(relativeParams.toQueryString());
}
}
}
std::string_view Uri::toString() const {
if (!this->_url) {
return std::string_view();
}
const std::string_view result = this->_url->get_href();
return this->_hasScheme ? result : result.substr(FILE_PREFIX.length());
}
bool Uri::isValid() const { return this->_url.has_value(); }
std::string_view Uri::getScheme() const {
if (!this->isValid()) {
return {};
}
return this->_hasScheme ? this->_url->get_protocol() : std::string_view{};
}
std::string_view Uri::getHost() const {
if (!this->isValid()) {
return {};
}
return this->_url->get_host();
}
std::string_view Uri::getQuery() const {
if (!this->isValid()) {
return {};
}
return this->_url->get_search();
}
std::string_view Uri::getPath() const {
if (!this->isValid()) {
return {};
}
// Remove leading '/'
return this->_url->get_pathname();
}
void Uri::setPath(const std::string_view& path) {
this->_url->set_pathname(path);
}
void Uri::setQuery(const std::string_view& queryString) {
this->_url->set_search(queryString);
}
std::string Uri::resolve(
const std::string& base,
const std::string& relative,
bool useBaseQuery,
bool assumeHttpsDefault) {
const std::string conformedBase = cesiumConformUrl(base, assumeHttpsDefault);
const std::string conformedRelative =
cesiumConformUrl(relative, assumeHttpsDefault);
UriUriA baseUri;
if (uriParseSingleUriA(&baseUri, conformedBase.c_str(), nullptr) !=
URI_SUCCESS) {
// Could not parse the base, so just use the relative directly and hope for
// the best.
return relative;
}
UriUriA relativeUri;
if (uriParseSingleUriA(&relativeUri, conformedRelative.c_str(), nullptr) !=
URI_SUCCESS) {
// Could not parse one of the URLs, so just use the relative directly and
// hope for the best.
uriFreeUriMembersA(&baseUri);
return relative;
}
UriUriA resolvedUri;
if (uriAddBaseUriA(&resolvedUri, &relativeUri, &baseUri) != URI_SUCCESS) {
uriFreeUriMembersA(&resolvedUri);
uriFreeUriMembersA(&relativeUri);
uriFreeUriMembersA(&baseUri);
return relative;
}
if (uriNormalizeSyntaxA(&resolvedUri) != URI_SUCCESS) {
uriFreeUriMembersA(&resolvedUri);
uriFreeUriMembersA(&relativeUri);
uriFreeUriMembersA(&baseUri);
return relative;
}
int charsRequired;
if (uriToStringCharsRequiredA(&resolvedUri, &charsRequired) != URI_SUCCESS) {
uriFreeUriMembersA(&resolvedUri);
uriFreeUriMembersA(&relativeUri);
uriFreeUriMembersA(&baseUri);
return relative;
}
std::string result(static_cast<size_t>(charsRequired), ' ');
if (uriToStringA(
const_cast<char*>(result.c_str()),
&resolvedUri,
charsRequired + 1,
nullptr) != URI_SUCCESS) {
uriFreeUriMembersA(&resolvedUri);
uriFreeUriMembersA(&relativeUri);
uriFreeUriMembersA(&baseUri);
return relative;
}
if (useBaseQuery) {
std::string query(baseUri.query.first, baseUri.query.afterLast);
if (query.length() > 0) {
if (resolvedUri.query.first) {
result += "&" + query;
} else {
result += "?" + query;
}
}
}
uriFreeUriMembersA(&resolvedUri);
uriFreeUriMembersA(&relativeUri);
uriFreeUriMembersA(&baseUri);
return result;
[[maybe_unused]] bool assumeHttpsDefault) {
return std::string(Uri(Uri(base), relative, useBaseQuery).toString());
}
std::string Uri::addQuery(
const std::string& uri,
const std::string& key,
const std::string& value) {
// TODO
if (uri.find('?') != std::string::npos) {
return uri + "&" + key + "=" + value;
Uri parsedUri(uri);
if (!parsedUri.isValid()) {
return uri;
}
return uri + "?" + key + "=" + value;
// UriUriA baseUri;
// if (uriParseSingleUriA(&baseUri, uri.c_str(), nullptr) != URI_SUCCESS)
//{
// // TODO: report error
// return uri;
//}
// uriFreeUriMembersA(&baseUri);
UriQuery params(parsedUri);
params.setValue(key, value);
parsedUri.setQuery(params.toQueryString());
return std::string(parsedUri.toString());
}
std::string Uri::getQueryValue(const std::string& url, const std::string& key) {
// We need to conform the URL since it will fail parsing if it's
// protocol-relative. However, it doesn't matter what protocol we use since
// it's only extracting query parameters.
const std::string conformedUrl = cesiumConformUrl(url, true);
UriUriA uri;
if (uriParseSingleUriA(&uri, conformedUrl.c_str(), nullptr) != URI_SUCCESS) {
return "";
std::string Uri::getQueryValue(const std::string& uri, const std::string& key) {
Uri parsedUri(uri);
if (!parsedUri.isValid()) {
return {};
}
UriQueryListA* queryList;
int itemCount;
if (uriDissectQueryMallocA(
&queryList,
&itemCount,
uri.query.first,
uri.query.afterLast) != URI_SUCCESS) {
uriFreeUriMembersA(&uri);
return "";
}
UriQueryListA* p = queryList;
while (p) {
if (p->key && std::strcmp(p->key, key.c_str()) == 0) {
std::string value = p->value ? p->value : "";
uriUnescapeInPlaceA(value.data());
uriFreeQueryListA(queryList);
uriFreeUriMembersA(&uri);
return value;
}
p = p->next;
}
uriFreeQueryListA(queryList);
uriFreeUriMembersA(&uri);
return "";
return std::string(UriQuery(parsedUri).getValue(key).value_or(""));
}
// NOLINTEND(bugprone-unchecked-optional-access)
std::string Uri::substituteTemplateParameters(
const std::string& templateUri,
const std::function<SubstitutionCallbackSignature>& substitutionCallback) {
@ -179,7 +200,10 @@ std::string Uri::substituteTemplateParameters(
++nextPos;
const size_t endPos = templateUri.find('}', nextPos);
if (endPos == std::string::npos) {
throw std::runtime_error("Unclosed template parameter");
// It's not a properly closed placeholder, so let's just output the rest
// of the URL (including the open brace) as-is and bail.
startPos = nextPos - 1;
break;
}
placeholder = templateUri.substr(nextPos, endPos - nextPos);
@ -194,240 +218,99 @@ std::string Uri::substituteTemplateParameters(
}
std::string Uri::escape(const std::string& s) {
// In the worst case, escaping causes each character to turn into three.
std::string result(s.size() * 3, '\0');
char* pTerminator = uriEscapeExA(
s.data(),
s.data() + s.size(),
result.data(),
URI_FALSE,
URI_FALSE);
result.resize(size_t(pTerminator - result.data()));
return result;
return ada::unicode::percent_encode(
s,
ada::character_sets::WWW_FORM_URLENCODED_PERCENT_ENCODE);
}
std::string Uri::unescape(const std::string& s) {
std::string result = s;
const char* pNewNull =
uriUnescapeInPlaceExA(result.data(), URI_FALSE, URI_BR_DONT_TOUCH);
result.resize(size_t(pNewNull - result.data()));
return result;
return ada::unicode::percent_decode(s, s.find('%'));
}
std::string Uri::unixPathToUriPath(const std::string& unixPath) {
// UriParser docs:
// The destination buffer must be large enough to hold 7 + 3 * len(filename)
// + 1 characters in case of an absolute filename or 3 * len(filename) + 1
// in case of a relative filename.
std::string result(7 + 3 * unixPath.size() + 1, '\0');
if (uriUnixFilenameToUriStringA(unixPath.data(), result.data()) != 0) {
// Error - return original string.
return unixPath;
} else {
// An absolute URI will start with "file://". Remove this.
if (result.find("file://", 0, 7) != std::string::npos) {
result.erase(0, 7);
}
// Truncate at first null character
result.resize(std::strlen(result.data()));
return result;
}
return Uri::nativePathToUriPath(unixPath);
}
std::string Uri::windowsPathToUriPath(const std::string& windowsPath) {
// uriWindowsFilenameToUriStringA doesn't allow `/` character in the path (it
// percent encodes them) even though that's a perfectly valid path separator
// on Windows. So convert all forward slashes to back slashes before calling
// it.
std::string windowsPathClean;
windowsPathClean.resize(windowsPath.size());
std::replace_copy(
windowsPath.begin(),
windowsPath.end(),
windowsPathClean.begin(),
'/',
'\\');
// UriParser docs:
// The destination buffer must be large enough to hold 8 + 3 * len(filename)
// + 1 characters in case of an absolute filename or 3 * len(filename) + 1
// in case of a relative filename.
std::string result(8 + 3 * windowsPathClean.size() + 1, '\0');
if (uriWindowsFilenameToUriStringA(windowsPathClean.data(), result.data()) !=
0) {
// Error - return original string.
return windowsPath;
} else {
// An absolute URI will start with "file://". Remove this.
if (result.find("file://", 0, 7) != std::string::npos) {
result.erase(0, 7);
}
// Truncate at first null character
result.resize(std::strlen(result.data()));
return result;
}
return Uri::nativePathToUriPath(windowsPath);
}
std::string Uri::nativePathToUriPath(const std::string& nativePath) {
#ifdef _WIN32
return windowsPathToUriPath(nativePath);
#else
return unixPathToUriPath(nativePath);
#endif
const std::string encoded = ada::unicode::percent_encode(
nativePath,
ada::character_sets::PATH_PERCENT_ENCODE);
const bool startsWithDriveLetter =
encoded.length() >= 2 &&
isAsciiAlpha(static_cast<unsigned char>(encoded[0])) && encoded[1] == ':';
std::string output;
output.reserve(encoded.length() + (startsWithDriveLetter ? 1 : 0));
// Paths like C:/... should be prefixed with a path separator
if (startsWithDriveLetter) {
output += PATH_SEP;
}
// All we really need to do from here is convert our slashes
for (size_t i = 0; i < encoded.length(); i++) {
if (encoded[i] == WINDOWS_PATH_SEP) {
output += PATH_SEP;
} else {
output += encoded[i];
}
}
return output;
}
std::string Uri::uriPathToUnixPath(const std::string& uriPath) {
// UriParser docs:
// The destination buffer must be large enough to hold len(uriString) + 1
// - 5 characters in case of an absolute URI or len(uriString) + 1 in case
// of a relative URI.
// However, the above seems to assume that uriPath starts with "file:", which
// is not required.
std::string result(uriPath.size() + 1, '\0');
if (uriUriStringToUnixFilenameA(uriPath.data(), result.data()) != 0) {
// Error - return original string.
return uriPath;
} else {
// Truncate at first null character
result.resize(std::strlen(result.data()));
return result;
}
// URI paths are pretty much just unix paths with URL encoding
const std::string_view& rawPath = uriPath;
return ada::unicode::percent_decode(rawPath, rawPath.find('%'));
}
std::string Uri::uriPathToWindowsPath(const std::string& uriPath) {
// If the URI starts with `/c:` or similar, remove the initial slash.
size_t skip = 0;
if (uriPath.size() >= 3 && uriPath[0] == '/' && uriPath[1] != '/' &&
uriPath[2] == ':') {
skip = 1;
const std::string path =
ada::unicode::percent_decode(uriPath, uriPath.find('%'));
size_t i = 0;
// A path including a drive name will start like /C:/....
// In that case, we just skip the first slash and continue on
if (path.length() >= 3 && path[0] == '/' &&
isAsciiAlpha(static_cast<unsigned char>(path[1])) && path[2] == ':') {
i++;
}
// UriParser docs:
// The destination buffer must be large enough to hold len(uriString) + 1
// - 5 characters in case of an absolute URI or len(uriString) + 1 in case
// of a relative URI.
// However, the above seems to assume that uriPath starts with "file:", which
// is not required.
std::string result(uriPath.size() + 1, '\0');
if (uriUriStringToWindowsFilenameA(uriPath.data() + skip, result.data()) !=
0) {
// Error - return original string.
return uriPath;
} else {
// Truncate at first null character
result.resize(std::strlen(result.data()));
return result;
std::string output;
output.reserve(path.length() - i);
for (; i < path.length(); i++) {
if (path[i] == PATH_SEP) {
output += WINDOWS_PATH_SEP;
} else {
output += path[i];
}
}
return output;
}
std::string Uri::uriPathToNativePath(const std::string& nativePath) {
std::string Uri::uriPathToNativePath(const std::string& uriPath) {
#ifdef _WIN32
return uriPathToWindowsPath(nativePath);
return uriPathToWindowsPath(uriPath);
#else
return uriPathToUnixPath(nativePath);
return uriPathToUnixPath(uriPath);
#endif
}
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<std::string> parts{std::string()};
UriPathSegmentA* pCurrent = parsedUri.pathHead;
while (pCurrent != nullptr) {
parts.emplace_back(
pCurrent->text.first,
size_t(pCurrent->text.afterLast - pCurrent->text.first));
pCurrent = pCurrent->next;
}
uriFreeUriMembersA(&parsedUri);
return joinToString(parts, "/");
return std::string(Uri(uri).getPath());
}
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<UriPathSegmentA*>(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<size_t>(charsRequired), ' ');
if (uriToStringA(
const_cast<char*>(result.c_str()),
&parsedUri,
charsRequired + 1,
nullptr) != URI_SUCCESS) {
uriFreeUriMembersA(&parsedUri);
return uri;
}
return result;
Uri parsedUri(uri);
parsedUri.setPath(newPath);
return std::string(parsedUri.toString());
}
} // namespace CesiumUtility

View File

@ -2,6 +2,9 @@
#include <doctest/doctest.h>
#include <map>
#include <string>
using namespace CesiumUtility;
TEST_CASE("Uri::getPath") {
@ -21,23 +24,34 @@ TEST_CASE("Uri::getPath") {
"/foo/bar/");
}
SUBCASE("returns empty path for nonexistent paths") {
CHECK(Uri::getPath("https://example.com") == "");
CHECK(Uri::getPath("https://example.com?some=parameter") == "");
SUBCASE("returns / path for nonexistent paths") {
CHECK(Uri::getPath("https://example.com") == "/");
CHECK(Uri::getPath("https://example.com?some=parameter") == "/");
}
SUBCASE("returns empty path for invalid uri") {
CHECK(Uri::getPath("not a valid uri") == "");
SUBCASE("handles unicode characters") {
CHECK(Uri::getPath("http://example.com/🐶.bin") == "/%F0%9F%90%B6.bin");
CHECK(
Uri::getPath("http://example.com/示例测试用例") ==
"/%E7%A4%BA%E4%BE%8B%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B");
CHECK(
Uri::getPath("http://example.com/Ῥόδος") ==
"/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82");
CHECK(
Uri::getPath(
"http://example.com/🙍‍♂️🚪🤚/🪝🚗🚪/❓📞") ==
"/%F0%9F%99%8D%E2%80%8D%E2%99%82%EF%B8%8F%F0%9F%9A%AA%F0%9F%A4%9A/"
"%F0%9F%AA%9D%F0%9F%9A%97%F0%9F%9A%AA/%E2%9D%93%F0%9F%93%9E");
}
}
TEST_CASE("Uri::setPath") {
SUBCASE("sets empty path") {
CHECK(Uri::setPath("https://example.com", "") == "https://example.com");
CHECK(Uri::setPath("https://example.com/", "") == "https://example.com/");
}
SUBCASE("sets new path") {
CHECK(Uri::setPath("https://example.com", "/") == "https://example.com/");
CHECK(Uri::setPath("https://example.com/", "/") == "https://example.com/");
CHECK(
Uri::setPath("https://example.com/foo", "/bar") ==
"https://example.com/bar");
@ -52,7 +66,7 @@ TEST_CASE("Uri::setPath") {
SUBCASE("preserves path parameters") {
CHECK(
Uri::setPath("https://example.com?some=parameter", "") ==
"https://example.com?some=parameter");
"https://example.com/?some=parameter");
CHECK(
Uri::setPath("https://example.com?some=parameter", "/") ==
"https://example.com/?some=parameter");
@ -77,8 +91,14 @@ TEST_CASE("Uri::setPath") {
"/foo/bar") == "https://example.com/foo/bar?some=parameter");
}
SUBCASE("returns empty path for invalid uri") {
CHECK(Uri::setPath("not a valid uri", "/foo/") == "");
SUBCASE("handles unicode characters") {
CHECK(
Uri::setPath("http://example.com/foo/", "/🐶.bin") ==
"http://example.com/%F0%9F%90%B6.bin");
CHECK(
Uri::setPath("http://example.com/bar/", "/示例测试用例") ==
"http://example.com/"
"%E7%A4%BA%E4%BE%8B%E6%B5%8B%E8%AF%95%E7%94%A8%E4%BE%8B");
}
}
@ -94,7 +114,10 @@ TEST_CASE("Uri::resolve") {
"//www.example.com",
"/page/test",
false,
false) == "http://www.example.com/page/test");
true) == "https://www.example.com/page/test");
CHECK(
CesiumUtility::Uri::resolve("https://www.example.com/", "/Ῥόδος") ==
"https://www.example.com/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82");
}
TEST_CASE("Uri::escape") {
@ -114,7 +137,7 @@ TEST_CASE("Uri::unixPathToUriPath") {
CHECK(Uri::unixPathToUriPath("wat") == "wat");
CHECK(Uri::unixPathToUriPath("wat/the") == "wat/the");
CHECK(Uri::unixPathToUriPath("/foo/bar") == "/foo/bar");
CHECK(Uri::unixPathToUriPath("/some:file") == "/some%3Afile");
CHECK(Uri::unixPathToUriPath("/some:file") == "/some:file");
CHECK(Uri::unixPathToUriPath("/🤞/😱/") == "/%F0%9F%A4%9E/%F0%9F%98%B1/");
}
@ -124,16 +147,16 @@ TEST_CASE("Uri::windowsPathToUriPath") {
CHECK(Uri::windowsPathToUriPath("wat") == "wat");
CHECK(Uri::windowsPathToUriPath("/foo/bar") == "/foo/bar");
CHECK(Uri::windowsPathToUriPath("d:\\foo/bar\\") == "/d:/foo/bar/");
CHECK(Uri::windowsPathToUriPath("e:\\some:file") == "/e:/some%3Afile");
CHECK(Uri::windowsPathToUriPath("e:\\some:file") == "/e:/some:file");
CHECK(
Uri::windowsPathToUriPath("c:/🤞/😱/") ==
"/c:/%F0%9F%A4%9E/%F0%9F%98%B1/");
CHECK(
Uri::windowsPathToUriPath("notadriveletter:\\file") ==
"notadriveletter%3A/file");
"notadriveletter:/file");
CHECK(
Uri::windowsPathToUriPath("\\notadriveletter:\\file") ==
"/notadriveletter%3A/file");
"/notadriveletter:/file");
}
TEST_CASE("Uri::uriPathToUnixPath") {
@ -158,3 +181,75 @@ TEST_CASE("Uri::uriPathToWindowsPath") {
Uri::uriPathToWindowsPath("/notadriveletter:/file") ==
"\\notadriveletter:\\file");
}
TEST_CASE("Uri::addQuery") {
CHECK(
Uri::addQuery("https://example.com/", "a", "1") ==
"https://example.com/?a=1");
CHECK(
Uri::addQuery("https://example.com/?a=1", "b", "2") ==
"https://example.com/?a=1&b=2");
CHECK(
Uri::addQuery("https://example.com/?a=1", "a", "2") ==
"https://example.com/?a=2");
CHECK(
Uri::addQuery("https://unparseable url", "a", "1") ==
"https://unparseable url");
CHECK(
Uri::addQuery("https://example.com/", "a", "!@#$%^&()_+{}|") ==
"https://example.com/?a=%21%40%23%24%25%5E%26%28%29_%2B%7B%7D%7C");
}
TEST_CASE("Uri::substituteTemplateParameters") {
const std::map<std::string, std::string> params{
{"a", "aValue"},
{"b", "bValue"},
{"c", "cValue"},
{"s", "teststr"},
{"one", "1"}};
const auto substitutionCallback = [&params](const std::string& placeholder) {
auto it = params.find(placeholder);
return it == params.end() ? placeholder : it->second;
};
CHECK(
Uri::substituteTemplateParameters(
"https://example.com/{a}/{b}/{c}",
substitutionCallback) == "https://example.com/aValue/bValue/cValue");
CHECK(
Uri::substituteTemplateParameters(
"https://example.com/enco%24d%5Ee%2Fd{s}tr1n%25g",
[]([[maybe_unused]] const std::string& placeholder) {
return "teststr";
}) == "https://example.com/enco%24d%5Ee%2Fdteststrtr1n%25g");
CHECK(
Uri::substituteTemplateParameters(
"https://example.com/{a",
substitutionCallback) == "https://example.com/{a");
CHECK(
Uri::substituteTemplateParameters(
"https://example.com/{}",
substitutionCallback) == "https://example.com/");
CHECK(
Uri::substituteTemplateParameters(
"https://example.com/a}",
substitutionCallback) == "https://example.com/a}");
}
TEST_CASE("UriQuery") {
SUBCASE("preserves placeholders") {
Uri uri("https://example.com?query={whatever}&{this}={that}");
UriQuery query(uri);
CHECK(query.getValue("query") == "{whatever}");
CHECK(query.getValue("{this}") == "{that}");
query.setValue("query", "foo");
query.setValue("{this}", "{another}");
CHECK(query.getValue("query") == "foo");
CHECK(query.getValue("{this}") == "{another}");
CHECK(query.toQueryString() == "query=foo&%7Bthis%7D=%7Banother%7D");
}
}

View File

@ -1,4 +1,10 @@
[
{
"name": "ada",
"url": "https://github.com/ada-url/ada",
"version": "2.9.2",
"license": ["Apache 2.0"]
},
{
"name": "Async++",
"url": "https://github.com/Amanieu/asyncplusplus",
@ -114,12 +120,6 @@
"version": "1aeb57d26bc303d5cfa1a9ff2a331df7ba278656",
"license": ["zlib"]
},
{
"name": "uriparser",
"url": "https://github.com/uriparser/uriparser",
"version": "0.9.6",
"license": ["BSD-3-Clause"]
},
{
"name": "zlib",
"url": "https://github.com/madler/zlib",

View File

@ -23,6 +23,5 @@ graph TD
Cesium3DTilesSelection[Cesium3DTilesSelection] --> spdlog_spdlog{{spdlog::spdlog}}
Cesium3DTilesSelection[Cesium3DTilesSelection] --> spdlog_spdlog_header_only{{spdlog::spdlog_header_only}}
Cesium3DTilesSelection[Cesium3DTilesSelection] --> tinyxml2_tinyxml2{{tinyxml2::tinyxml2}}
Cesium3DTilesSelection[Cesium3DTilesSelection] --> uriparser_uriparser{{uriparser::uriparser}}
class draco_draco,libmorton_libmorton,nonstd_expected-lite,spdlog_spdlog,spdlog_spdlog_header_only,tinyxml2_tinyxml2,uriparser_uriparser dependencyNode
class draco_draco,libmorton_libmorton,nonstd_expected-lite,spdlog_spdlog,spdlog_spdlog_header_only,tinyxml2_tinyxml2 dependencyNode
class Cesium3DTiles,Cesium3DTilesContent,Cesium3DTilesReader,CesiumAsync,CesiumGeometry,CesiumGeospatial,CesiumGltf,CesiumGltfReader,CesiumQuantizedMeshTerrain,CesiumRasterOverlays,CesiumUtility,Cesium3DTilesSelection libraryNode

View File

@ -7,10 +7,11 @@ graph TD
classDef dependencyNode fill:#fff,stroke:#ccc,color:#666,font-weight:bold,font-size:28px
classDef libraryNode fill:#9f9,font-weight:bold,font-size:28px
CesiumIonClient[CesiumIonClient] --> CesiumAsync[CesiumAsync]
CesiumIonClient[CesiumIonClient] --> CesiumGeospatial[CesiumGeospatial]
CesiumIonClient[CesiumIonClient] --> CesiumUtility[CesiumUtility]
CesiumIonClient[CesiumIonClient] --> OpenSSL_Crypto{{OpenSSL::Crypto}}
CesiumIonClient[CesiumIonClient] --> httplib_httplib{{httplib::httplib}}
CesiumIonClient[CesiumIonClient] --> modp_b64_modp_b64{{modp_b64::modp_b64}}
CesiumIonClient[CesiumIonClient] --> picosha2_picosha2{{picosha2::picosha2}}
class OpenSSL_Crypto,httplib_httplib,modp_b64_modp_b64,picosha2_picosha2 dependencyNode
class CesiumAsync,CesiumUtility,CesiumIonClient libraryNode
class CesiumAsync,CesiumGeospatial,CesiumUtility,CesiumIonClient libraryNode

View File

@ -6,9 +6,9 @@ title: CesiumUtility Dependency Graph
graph TD
classDef dependencyNode fill:#fff,stroke:#ccc,color:#666,font-weight:bold,font-size:28px
classDef libraryNode fill:#9f9,font-weight:bold,font-size:28px
CesiumUtility[CesiumUtility] --> ada_ada{{ada::ada}}
CesiumUtility[CesiumUtility] --> glm_glm{{glm::glm}}
CesiumUtility[CesiumUtility] --> spdlog_spdlog{{spdlog::spdlog}}
CesiumUtility[CesiumUtility] --> uriparser_uriparser{{uriparser::uriparser}}
CesiumUtility[CesiumUtility] --> zlib-ng_zlib-ng{{zlib-ng::zlib-ng}}
class glm_glm,spdlog_spdlog,uriparser_uriparser,zlib-ng_zlib-ng dependencyNode
class ada_ada,glm_glm,spdlog_spdlog,zlib-ng_zlib-ng dependencyNode
class CesiumUtility libraryNode

View File

@ -7,9 +7,9 @@ config:
graph TD
classDef dependencyNode fill:#fff,stroke:#ccc,color:#666,font-weight:bold,font-size:28px
classDef libraryNode fill:#9f9,font-weight:bold,font-size:28px
CesiumUtility[CesiumUtility] --> ada_ada{{ada::ada}}
CesiumUtility[CesiumUtility] --> glm_glm{{glm::glm}}
CesiumUtility[CesiumUtility] --> spdlog_spdlog{{spdlog::spdlog}}
CesiumUtility[CesiumUtility] --> uriparser_uriparser{{uriparser::uriparser}}
CesiumUtility[CesiumUtility] --> zlib-ng_zlib-ng{{zlib-ng::zlib-ng}}
Cesium3DTiles[Cesium3DTiles] --> CesiumUtility[CesiumUtility]
Cesium3DTilesContent[Cesium3DTilesContent] --> Cesium3DTiles[Cesium3DTiles]
@ -70,7 +70,6 @@ graph TD
Cesium3DTilesSelection[Cesium3DTilesSelection] --> spdlog_spdlog{{spdlog::spdlog}}
Cesium3DTilesSelection[Cesium3DTilesSelection] --> spdlog_spdlog_header_only{{spdlog::spdlog_header_only}}
Cesium3DTilesSelection[Cesium3DTilesSelection] --> tinyxml2_tinyxml2{{tinyxml2::tinyxml2}}
Cesium3DTilesSelection[Cesium3DTilesSelection] --> uriparser_uriparser{{uriparser::uriparser}}
CesiumQuantizedMeshTerrain[CesiumQuantizedMeshTerrain] --> CesiumAsync[CesiumAsync]
CesiumQuantizedMeshTerrain[CesiumQuantizedMeshTerrain] --> CesiumGeospatial[CesiumGeospatial]
CesiumQuantizedMeshTerrain[CesiumQuantizedMeshTerrain] --> CesiumGltf[CesiumGltf]
@ -94,28 +93,29 @@ graph TD
CesiumGltfWriter[CesiumGltfWriter] --> CesiumJsonWriter[CesiumJsonWriter]
CesiumGltfWriter[CesiumGltfWriter] --> modp_b64_modp_b64{{modp_b64::modp_b64}}
CesiumIonClient[CesiumIonClient] --> CesiumAsync[CesiumAsync]
CesiumIonClient[CesiumIonClient] --> CesiumGeospatial[CesiumGeospatial]
CesiumIonClient[CesiumIonClient] --> CesiumUtility[CesiumUtility]
CesiumIonClient[CesiumIonClient] --> OpenSSL_Crypto{{OpenSSL::Crypto}}
CesiumIonClient[CesiumIonClient] --> httplib_httplib{{httplib::httplib}}
CesiumIonClient[CesiumIonClient] --> modp_b64_modp_b64{{modp_b64::modp_b64}}
CesiumIonClient[CesiumIonClient] --> picosha2_picosha2{{picosha2::picosha2}}
class glm_glm,spdlog_spdlog,uriparser_uriparser,zlib-ng_zlib-ng,libmorton_libmorton,Async_,spdlog_spdlog_header_only,unofficial_sqlite3_sqlite3,earcut,s2_s2,KTX_ktx,WebP_webp,WebP_webpdecoder,draco_draco,libjpeg-turbo_turbojpeg-static,meshoptimizer_meshoptimizer,modp_b64_modp_b64,nonstd_expected-lite,tinyxml2_tinyxml2,OpenSSL_Crypto,httplib_httplib,picosha2_picosha2 dependencyNode
class ada_ada,glm_glm,spdlog_spdlog,zlib-ng_zlib-ng,libmorton_libmorton,Async_,spdlog_spdlog_header_only,unofficial_sqlite3_sqlite3,earcut,s2_s2,KTX_ktx,WebP_webp,WebP_webpdecoder,draco_draco,libjpeg-turbo_turbojpeg-static,meshoptimizer_meshoptimizer,modp_b64_modp_b64,nonstd_expected-lite,tinyxml2_tinyxml2,OpenSSL_Crypto,httplib_httplib,picosha2_picosha2 dependencyNode
class CesiumUtility,Cesium3DTiles,Cesium3DTilesReader,CesiumAsync,CesiumGeometry,CesiumGeospatial,CesiumGltf,CesiumGltfContent,CesiumGltfReader,Cesium3DTilesContent,CesiumJsonReader,CesiumQuantizedMeshTerrain,CesiumRasterOverlays,Cesium3DTilesSelection,CesiumJsonWriter,Cesium3DTilesWriter,CesiumGltfWriter,CesiumIonClient libraryNode
linkStyle 0 stroke:#ff0029,stroke-width:8px
linkStyle 1,20,60 stroke:#377eb8,stroke-width:8px
linkStyle 2,63 stroke:#66a61e,stroke-width:8px
linkStyle 1 stroke:#377eb8,stroke-width:8px
linkStyle 2,20,60 stroke:#66a61e,stroke-width:8px
linkStyle 3 stroke:#984ea3,stroke-width:8px
linkStyle 4,13,19,23,24,26,29,35,56,70,71,78,87 stroke:#00d2d5,stroke-width:8px
linkStyle 5,15,46,81 stroke:#ff7f00,stroke-width:8px
linkStyle 4,13,19,23,24,26,29,35,56,69,70,77,87 stroke:#00d2d5,stroke-width:8px
linkStyle 5,15,46,80 stroke:#ff7f00,stroke-width:8px
linkStyle 6,48 stroke:#af8d00,stroke-width:8px
linkStyle 7,16,30,36,49,64,72,86 stroke:#7f80cd,stroke-width:8px
linkStyle 8,25,31,50,73 stroke:#b3e900,stroke-width:8px
linkStyle 9,32,51,65,74 stroke:#c42e60,stroke-width:8px
linkStyle 10,33,37,52,66,75,83 stroke:#a65628,stroke-width:8px
linkStyle 11,67,76 stroke:#f781bf,stroke-width:8px
linkStyle 12,34,53,77 stroke:#8dd3c7,stroke-width:8px
linkStyle 7,16,30,36,49,63,71,85 stroke:#7f80cd,stroke-width:8px
linkStyle 8,25,31,50,72 stroke:#b3e900,stroke-width:8px
linkStyle 9,32,51,64,73,86 stroke:#c42e60,stroke-width:8px
linkStyle 10,33,37,52,65,74,82 stroke:#a65628,stroke-width:8px
linkStyle 11,66,75 stroke:#f781bf,stroke-width:8px
linkStyle 12,34,53,76 stroke:#8dd3c7,stroke-width:8px
linkStyle 14,58 stroke:#bebada,stroke-width:8px
linkStyle 17,38,68 stroke:#fb8072,stroke-width:8px
linkStyle 17,38,67 stroke:#fb8072,stroke-width:8px
linkStyle 18 stroke:#80b1d3,stroke-width:8px
linkStyle 21,61 stroke:#fdb462,stroke-width:8px
linkStyle 22 stroke:#fccde5,stroke-width:8px
@ -127,13 +127,13 @@ graph TD
linkStyle 42,57 stroke:#d95f02,stroke-width:8px
linkStyle 43 stroke:#e7298a,stroke-width:8px
linkStyle 44 stroke:#e6ab02,stroke-width:8px
linkStyle 45,85,90 stroke:#a6761d,stroke-width:8px
linkStyle 45,84,90 stroke:#a6761d,stroke-width:8px
linkStyle 47 stroke:#0097ff,stroke-width:8px
linkStyle 54 stroke:#00d067,stroke-width:8px
linkStyle 55 stroke:#000000,stroke-width:8px
linkStyle 59,79 stroke:#252525,stroke-width:8px
linkStyle 62,80 stroke:#525252,stroke-width:8px
linkStyle 69,82,84 stroke:#737373,stroke-width:8px
linkStyle 59,78 stroke:#252525,stroke-width:8px
linkStyle 62,79 stroke:#525252,stroke-width:8px
linkStyle 68,81,83 stroke:#737373,stroke-width:8px
linkStyle 88 stroke:#969696,stroke-width:8px
linkStyle 89 stroke:#bdbdbd,stroke-width:8px
linkStyle 91 stroke:#f43600,stroke-width:8px

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -4,6 +4,7 @@ Cesium Native relies on a number of third-party dependencies. These dependencies
| Dependency | Usage |
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| [ada](https://github.com/ada-url/ada) | Used to parse and manipulate URIs. |
| [Async++](https://github.com/Amanieu/asyncplusplus) | Used by CesiumAsync for cross-platform concurrency. |
| [Catch2](https://github.com/catchorg/Catch2) | Test framework used by CesiumNativeTests. |
| [draco](https://github.com/google/draco) | Required to decode meshes and point clouds compressed with Draco. |
@ -25,7 +26,6 @@ Cesium Native relies on a number of third-party dependencies. These dependencies
| [sqlite3](https://www.sqlite.org/index.html) | Used to cache HTTP responses. |
| [stb_image](https://github.com/nothings/stb/blob/master/stb_image.h) | A simple image loader. |
| [tinyxml2](https://github.com/leethomason/tinyxml2) | XML parser for interacting with XML APIs such as those implementing the Web Map Service standard. |
| [uriparser](https://github.com/uriparser/uriparser) | Used to parse and manipulate URIs. |
| [zlib-ng](https://github.com/zlib-ng/zlib-ng) | An optimized zlib implementation for working with Gzipped data. |
The following chart illustrates the connections between the Cesium Native libraries and third-party dependencies:

View File

@ -1,6 +1,6 @@
{
"name": "cesium-native",
"version": "0.43.0",
"version": "0.44.0",
"description": "Cesium 3D Geospatial for C++",
"main": "index.js",
"directories": {