diff --git a/src/api/common/include/exchangebase.hpp b/src/api/common/include/exchangebase.hpp index dc082cdc..cded31d9 100644 --- a/src/api/common/include/exchangebase.hpp +++ b/src/api/common/include/exchangebase.hpp @@ -32,6 +32,8 @@ class CacheFreezerRAII { return *this; } + void swap(CacheFreezerRAII &rhs) noexcept { std::swap(_pCachedResultVault, rhs._pCachedResultVault); } + ~CacheFreezerRAII() { if (_pCachedResultVault != nullptr) { _pCachedResultVault->unfreezeAll(); diff --git a/src/api/exchanges/src/binanceprivateapi.cpp b/src/api/exchanges/src/binanceprivateapi.cpp index 5c17988b..4afdd225 100644 --- a/src/api/exchanges/src/binanceprivateapi.cpp +++ b/src/api/exchanges/src/binanceprivateapi.cpp @@ -108,7 +108,7 @@ void SetNonceAndSignature(const APIKey& apiKey, CurlPostData& postData, Duration postData.pop_back(); } - postData.push_back(kSignatureKey, ssl::ShaHex(ssl::ShaType::kSha256, postData.str(), apiKey.privateKey())); + postData.emplace_back(kSignatureKey, ssl::ShaHex(ssl::ShaType::kSha256, postData.str(), apiKey.privateKey())); } bool CheckErrorDoRetry(int statusCode, const json& ret, QueryDelayDir& queryDelayDir, Duration& sleepingTime, @@ -305,7 +305,7 @@ bool BinancePrivate::checkMarketAppendSymbol(Market mk, CurlPostData& params) { if (!optMarket) { return false; } - params.push_back("symbol", optMarket->assetsPairStrUpper()); + params.emplace_back("symbol", optMarket->assetsPairStrUpper()); return true; } @@ -381,10 +381,10 @@ ClosedOrderVector BinancePrivate::queryClosedOrders(const OrdersConstraints& clo return closedOrders; } if (closedOrdersConstraints.isPlacedTimeAfterDefined()) { - params.push_back("startTime", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedAfter())); + params.emplace_back("startTime", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedAfter())); } if (closedOrdersConstraints.isPlacedTimeBeforeDefined()) { - params.push_back("endTime", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedBefore())); + params.emplace_back("endTime", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedBefore())); } const json result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/allOrders", _queryDelay, std::move(params)); @@ -485,17 +485,17 @@ DepositsSet BinancePrivate::queryRecentDeposits(const DepositsConstraints& depos Deposits deposits; CurlPostData options; if (depositsConstraints.isCurDefined()) { - options.push_back("coin", depositsConstraints.currencyCode().str()); + options.emplace_back("coin", depositsConstraints.currencyCode().str()); } if (depositsConstraints.isTimeAfterDefined()) { - options.push_back("startTime", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeAfter())); + options.emplace_back("startTime", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeAfter())); } if (depositsConstraints.isTimeBeforeDefined()) { - options.push_back("endTime", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeBefore())); + options.emplace_back("endTime", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeBefore())); } if (depositsConstraints.isIdDefined()) { if (depositsConstraints.idSet().size() == 1) { - options.push_back("txId", depositsConstraints.idSet().front()); + options.emplace_back("txId", depositsConstraints.idSet().front()); } } json depositStatus = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/sapi/v1/capital/deposit/hisrec", @@ -574,13 +574,13 @@ TimePoint RetrieveTimeStampFromWithdrawJson(const json& withdrawJson) { CurlPostData CreateOptionsFromWithdrawConstraints(const WithdrawsConstraints& withdrawsConstraints) { CurlPostData options; if (withdrawsConstraints.isCurDefined()) { - options.push_back("coin", withdrawsConstraints.currencyCode().str()); + options.emplace_back("coin", withdrawsConstraints.currencyCode().str()); } if (withdrawsConstraints.isTimeAfterDefined()) { - options.push_back("startTime", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeAfter())); + options.emplace_back("startTime", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeAfter())); } if (withdrawsConstraints.isTimeBeforeDefined()) { - options.push_back("endTime", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeBefore())); + options.emplace_back("endTime", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeBefore())); } return options; } @@ -719,8 +719,8 @@ PlaceOrderInfo BinancePrivate::placeOrder(MonetaryAmount from, MonetaryAmount vo {"symbol", mk.assetsPairStrUpper()}, {"side", buyOrSell}, {"type", orderType}, {"quantity", volume.amountStr()}}; if (!isTakerStrategy) { - placePostData.push_back("timeInForce", "GTC"); - placePostData.push_back("price", price.amountStr()); + placePostData.emplace_back("timeInForce", "GTC"); + placePostData.emplace_back("price", price.amountStr()); } const std::string_view methodName = isSimulation ? "/api/v3/order/test" : "/api/v3/order"; @@ -768,7 +768,7 @@ OrderInfo BinancePrivate::queryOrder(OrderIdView orderId, const TradeContext& tr CurlPostData myTradesOpts{{"symbol", assets}}; auto timeIt = result.find("time"); if (timeIt != result.end()) { - myTradesOpts.push_back("startTime", timeIt->get() - 100L); // -100 just to be sure + myTradesOpts.emplace_back("startTime", timeIt->get() - 100L); // -100 just to be sure } result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/api/v3/myTrades", _queryDelay, myTradesOpts); int64_t integralOrderId = FromString(orderId); @@ -786,7 +786,7 @@ InitiatedWithdrawInfo BinancePrivate::launchWithdraw(MonetaryAmount grossAmount, CurlPostData withdrawPostData{ {"coin", currencyCode.str()}, {"address", destinationWallet.address()}, {"amount", grossAmount.amountStr()}}; if (destinationWallet.hasTag()) { - withdrawPostData.push_back("addressTag", destinationWallet.tag()); + withdrawPostData.emplace_back("addressTag", destinationWallet.tag()); } json result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, "/sapi/v1/capital/withdraw/apply", _queryDelay, std::move(withdrawPostData)); diff --git a/src/api/exchanges/src/bithumbprivateapi.cpp b/src/api/exchanges/src/bithumbprivateapi.cpp index 0f175430..bdec3272 100644 --- a/src/api/exchanges/src/bithumbprivateapi.cpp +++ b/src/api/exchanges/src/bithumbprivateapi.cpp @@ -297,7 +297,8 @@ template json PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, std::string_view endpoint, CurlPostDataT&& curlPostData = CurlPostData()) { CurlPostData postData(std::forward(curlPostData)); - postData.push_front("endpoint", endpoint); + postData.emplace_front("endpoint", endpoint); + CurlOptions opts(HttpRequestType::kPost, postData.urlEncodeExceptDelimiters()); return PrivateQueryProcessWithRetries(curlHandle, apiKey, endpoint, std::move(opts)); @@ -354,12 +355,12 @@ BalancePortfolio BithumbPrivate::queryAccountBalance(const BalanceOptions& balan json jsonReply = PrivateQuery(_curlHandle, _apiKey, "/info/balance", {{"currency", "all"}}); BalancePortfolio balancePortfolio; - - const auto balanceItemsIt = jsonReply.find("data"); if (jsonReply.is_discarded()) { log::error("Badly formatted {} reply from balance", exchangeName()); return balancePortfolio; } + + const auto balanceItemsIt = jsonReply.find("data"); if (balanceItemsIt == jsonReply.end()) { return balancePortfolio; } @@ -448,7 +449,7 @@ auto FillOrderCurrencies(const OrdersConstraints& ordersConstraints, ExchangePub if (!filterMarket.base().isNeutral()) { orderCurrencies.push_back(filterMarket.base()); if (!filterMarket.quote().isNeutral()) { - params.push_back(kPaymentCurParamStr, filterMarket.quote().str()); + params.emplace_back(kPaymentCurParamStr, filterMarket.quote().str()); } } } else { @@ -496,7 +497,7 @@ OrderVectorType QueryOrders(const OrdersConstraints& ordersConstraints, Exchange OrderVectorType orders; if (ordersConstraints.isPlacedTimeAfterDefined()) { - params.push_back("after", TimestampToMillisecondsSinceEpoch(ordersConstraints.placedAfter())); + params.emplace_back("after", TimestampToMillisecondsSinceEpoch(ordersConstraints.placedAfter())); } if (orderCurrencies.size() > 1) { if constexpr (std::is_same_v) { @@ -659,16 +660,16 @@ json QueryUserTransactions(BithumbPrivate& exchangePrivate, CurlHandle& curlHand if (userTransactionEnum == UserTransactionEnum::kClosedOrders) { if constexpr (std::is_same_v) { if (constraints.isCur2Defined()) { - options.push_back(kPaymentCurParamStr, constraints.curStr2()); + options.emplace_back(kPaymentCurParamStr, constraints.curStr2()); } else { - options.push_back(kPaymentCurParamStr, "KRW"); + options.emplace_back(kPaymentCurParamStr, "KRW"); } } } else { // It's not clear what the payment currency option is for user_transactions endpoint for deposits and withdraws. // For withdraws it seems to have no impact, and even worse, it returns weird output when // querying withdraws for a specific coin, it can return KRW withdraws to user bank account. - options.push_back(kPaymentCurParamStr, "BTC"); + options.emplace_back(kPaymentCurParamStr, "BTC"); } json allResults; @@ -857,8 +858,8 @@ PlaceOrderInfo BithumbPrivate::placeOrder(MonetaryAmount /*from*/, MonetaryAmoun endpoint.append(fromCurrencyCode == mk.base() ? "market_sell" : "market_buy"); } else { endpoint.append("place"); - placePostData.push_back(kTypeParamStr, orderType); - placePostData.push_back("price", price.amountStr()); + placePostData.emplace_back(kTypeParamStr, orderType); + placePostData.emplace_back("price", price.amountStr()); } // Volume is gross amount if from amount is in quote currency, we should remove the fees @@ -933,7 +934,7 @@ PlaceOrderInfo BithumbPrivate::placeOrder(MonetaryAmount /*from*/, MonetaryAmoun } } - placePostData.push_back("units", volume.amountStr()); + placePostData.emplace_back("units", volume.amountStr()); placeOrderInfo.setClosed(); @@ -1025,10 +1026,10 @@ CurlPostData OrderInfoPostData(Market mk, TradeSide side, OrderIdView orderId) { ret.underlyingBufferReserve(kOrderCurrencyParamStr.size() + kPaymentCurParamStr.size() + kTypeParamStr.size() + kOrderIdParamStr.size() + baseStr.size() + quoteStr.size() + orderId.size() + 10U); - ret.push_back(kOrderCurrencyParamStr, baseStr); - ret.push_back(kPaymentCurParamStr, quoteStr); - ret.push_back(kTypeParamStr, side == TradeSide::kSell ? "ask" : "bid"); - ret.push_back(kOrderIdParamStr, orderId); + ret.emplace_back(kOrderCurrencyParamStr, baseStr); + ret.emplace_back(kPaymentCurParamStr, quoteStr); + ret.emplace_back(kTypeParamStr, side == TradeSide::kSell ? "ask" : "bid"); + ret.emplace_back(kOrderIdParamStr, orderId); return ret; } @@ -1116,14 +1117,14 @@ CurlPostData ComputeLaunchWithdrawCurlPostData(MonetaryAmount netEmittedAmount, // coincenter can retrieve the account owner name automatically provided that the user filled the fields in the // destination api key part in the secrets json file. if (desAccountOwner.isFullyDefined()) { - withdrawPostData.push_back("en_name", desAccountOwner.enName()); - withdrawPostData.push_back("ko_name", desAccountOwner.koName()); + withdrawPostData.emplace_back("en_name", desAccountOwner.enName()); + withdrawPostData.emplace_back("ko_name", desAccountOwner.koName()); } else { log::error("Bithumb withdrawal needs further information for destination account"); log::error("it needs the English and Korean name of its owner so query will most probably fail"); } if (destinationWallet.hasTag()) { - withdrawPostData.push_back("destination", destinationWallet.tag()); + withdrawPostData.emplace_back("destination", destinationWallet.tag()); } return withdrawPostData; } diff --git a/src/api/exchanges/src/huobiprivateapi.cpp b/src/api/exchanges/src/huobiprivateapi.cpp index a9aa4980..b028dec0 100644 --- a/src/api/exchanges/src/huobiprivateapi.cpp +++ b/src/api/exchanges/src/huobiprivateapi.cpp @@ -47,6 +47,7 @@ #include "tradedamounts.hpp" #include "tradeinfo.hpp" #include "tradeside.hpp" +#include "url-encode.hpp" #include "wallet.hpp" #include "withdraw.hpp" #include "withdrawinfo.hpp" @@ -81,7 +82,9 @@ CurlOptions::PostDataFormat ComputePostDataFormat(HttpRequestType requestType, c void SetNonceAndSignature(CurlHandle& curlHandle, const APIKey& apiKey, HttpRequestType requestType, std::string_view endpoint, CurlPostData& postData, CurlPostData& signaturePostData) { - signaturePostData.set("Timestamp", URLEncode(Nonce_LiteralDate(kTimeYearToSecondTSeparatedFormat))); + auto isNotEncoded = [](char ch) { return isalnum(ch) || ch == '-' || ch == '.' || ch == '_' || ch == '~'; }; + + signaturePostData.set("Timestamp", URLEncode(Nonce_LiteralDate(kTimeYearToSecondTSeparatedFormat), isNotEncoded)); if (!postData.empty() && requestType == HttpRequestType::kGet) { // Warning: Huobi expects that all parameters of the query are ordered lexicographically @@ -98,11 +101,12 @@ void SetNonceAndSignature(CurlHandle& curlHandle, const APIKey& apiKey, HttpRequ signaturePostData.pop_back(); } - signaturePostData.push_back( - kSignatureKey, URLEncode(B64Encode(ssl::ShaBin( - ssl::ShaType::kSha256, - BuildParamStr(requestType, curlHandle.getNextBaseUrl(), endpoint, signaturePostData.str()), - apiKey.privateKey())))); + signaturePostData.emplace_back( + kSignatureKey, URLEncode(B64Encode(ssl::ShaBin(ssl::ShaType::kSha256, + BuildParamStr(requestType, curlHandle.getNextBaseUrl(), endpoint, + signaturePostData.str()), + apiKey.privateKey())), + isNotEncoded)); } json PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, HttpRequestType requestType, std::string_view endpoint, @@ -241,19 +245,19 @@ ClosedOrderVector HuobiPrivate::queryClosedOrders(const OrdersConstraints& close CurlPostData params; if (closedOrdersConstraints.isPlacedTimeBeforeDefined()) { - params.push_back("end-time", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedBefore())); + params.emplace_back("end-time", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedBefore())); } if (closedOrdersConstraints.isPlacedTimeAfterDefined()) { - params.push_back("start-time", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedAfter())); + params.emplace_back("start-time", TimestampToMillisecondsSinceEpoch(closedOrdersConstraints.placedAfter())); } if (closedOrdersConstraints.isMarketDefined()) { // we can use the more detailed endpoint that requires the market // Do not ask for cancelled orders without any matched part - params.push_back("states", "filled"); + params.emplace_back("states", "filled"); - params.push_back("symbol", closedOrdersConstraints.market().assetsPairStrLower()); + params.emplace_back("symbol", closedOrdersConstraints.market().assetsPairStrLower()); } else { // Only past 48h orders may be retrieved without market } @@ -323,7 +327,7 @@ OpenedOrderVector HuobiPrivate::queryOpenedOrders(const OrdersConstraints& opene openedOrdersConstraints.cur2()); if (filterMarket.isDefined()) { - params.push_back("symbol", filterMarket.assetsPairStrLower()); + params.emplace_back("symbol", filterMarket.assetsPairStrLower()); } } @@ -411,10 +415,10 @@ DepositsSet HuobiPrivate::queryRecentDeposits(const DepositsConstraints& deposit Deposits deposits; CurlPostData options; if (depositsConstraints.isCurDefined()) { - options.push_back("currency", ToLower(depositsConstraints.currencyCode().str())); + options.emplace_back("currency", ToLower(depositsConstraints.currencyCode().str())); } - options.push_back("size", 500); - options.push_back("type", "deposit"); + options.emplace_back("size", 500); + options.emplace_back("type", "deposit"); json data = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kGet, "/v1/query/deposit-withdraw", std::move(options)); const auto dataIt = data.find("data"); @@ -533,10 +537,10 @@ Withdraw::Status WithdrawStatusFromStatusStr(std::string_view statusStr, bool lo CurlPostData CreateOptionsFromWithdrawConstraints(const WithdrawsConstraints& withdrawsConstraints) { CurlPostData options; if (withdrawsConstraints.isCurDefined()) { - options.push_back("currency", ToLower(withdrawsConstraints.currencyCode().str())); + options.emplace_back("currency", ToLower(withdrawsConstraints.currencyCode().str())); } - options.push_back("size", 500); - options.push_back("type", "withdraw"); + options.emplace_back("size", 500); + options.emplace_back("type", "withdraw"); return options; } } // namespace @@ -640,10 +644,10 @@ PlaceOrderInfo HuobiPrivate::placeOrder(MonetaryAmount from, MonetaryAmount volu placePostData.set("amount", from.amountStr()); } } else { - placePostData.push_back("price", price.amountStr()); + placePostData.emplace_back("price", price.amountStr()); } - placePostData.push_back("symbol", lowerCaseMarket); - placePostData.push_back("type", type); + placePostData.emplace_back("symbol", lowerCaseMarket); + placePostData.emplace_back("type", type); json result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, "/v1/order/orders/place", std::move(placePostData)); @@ -742,9 +746,9 @@ InitiatedWithdrawInfo HuobiPrivate::launchWithdraw(MonetaryAmount grossAmount, W CurlPostData withdrawPostData; if (destinationWallet.hasTag()) { - withdrawPostData.push_back("addr-tag", destinationWallet.tag()); + withdrawPostData.emplace_back("addr-tag", destinationWallet.tag()); } - withdrawPostData.push_back("address", destinationWallet.address()); + withdrawPostData.emplace_back("address", destinationWallet.address()); MonetaryAmount withdrawFee = _exchangePublic.queryWithdrawalFeeOrZero(currencyCode); HuobiPublic::WithdrawParams withdrawParams = huobiPublic.getWithdrawParams(currencyCode); @@ -764,10 +768,10 @@ InitiatedWithdrawInfo HuobiPrivate::launchWithdraw(MonetaryAmount grossAmount, W grossAmount.truncate(withdrawParams.withdrawPrecision); } - withdrawPostData.push_back("amount", netEmittedAmount.amountStr()); - withdrawPostData.push_back("currency", lowerCaseCur); + withdrawPostData.emplace_back("amount", netEmittedAmount.amountStr()); + withdrawPostData.emplace_back("currency", lowerCaseCur); // Strange to have the fee as input parameter of a withdraw... - withdrawPostData.push_back("fee", withdrawFee.amountStr()); + withdrawPostData.emplace_back("fee", withdrawFee.amountStr()); json result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, "/v1/dw/withdraw/api/create", std::move(withdrawPostData)); diff --git a/src/api/exchanges/src/huobipublicapi.cpp b/src/api/exchanges/src/huobipublicapi.cpp index 69d07de4..d2b64d82 100644 --- a/src/api/exchanges/src/huobipublicapi.cpp +++ b/src/api/exchanges/src/huobipublicapi.cpp @@ -384,7 +384,7 @@ MarketOrderBook HuobiPublic::OrderBookFunc::operator()(Market mk, int depth) { if (lb == kAuthorizedDepths.end()) { log::warn("Invalid depth {}, default to {}", depth, kHuobiStandardOrderBookDefaultDepth); } else if (*lb != kHuobiStandardOrderBookDefaultDepth) { - postData.push_back("depth", *lb); + postData.emplace_back("depth", *lb); } } diff --git a/src/api/exchanges/src/krakenprivateapi.cpp b/src/api/exchanges/src/krakenprivateapi.cpp index dbf355b6..f360ebb6 100644 --- a/src/api/exchanges/src/krakenprivateapi.cpp +++ b/src/api/exchanges/src/krakenprivateapi.cpp @@ -85,7 +85,7 @@ std::pair PrivateQuery(CurlHandle& curlHandle, const APIK CurlOptions opts(HttpRequestType::kPost, std::forward(curlPostData)); Nonce nonce = Nonce_TimeSinceEpochInMs(); - opts.mutablePostData().push_back("nonce", nonce); + opts.mutablePostData().emplace_back("nonce", nonce); opts.appendHttpHeader("API-Key", apiKey.key()); opts.appendHttpHeader("API-Sign", PrivateSignature(apiKey, path, nonce, opts.postData().str())); @@ -294,10 +294,10 @@ ClosedOrderVector KrakenPrivate::queryClosedOrders(const OrdersConstraints& clos CurlPostData params{{"ofs", page}, {"trades", "true"}}; if (closedOrdersConstraints.isPlacedTimeAfterDefined()) { - params.push_back("start", TimestampToSecondsSinceEpoch(closedOrdersConstraints.placedAfter())); + params.emplace_back("start", TimestampToSecondsSinceEpoch(closedOrdersConstraints.placedAfter())); } if (closedOrdersConstraints.isPlacedTimeBeforeDefined()) { - params.push_back("end", TimestampToSecondsSinceEpoch(closedOrdersConstraints.placedBefore())); + params.emplace_back("end", TimestampToSecondsSinceEpoch(closedOrdersConstraints.placedBefore())); } static constexpr int kLimitNbOrdersPerPage = 50; @@ -446,7 +446,7 @@ DepositsSet KrakenPrivate::queryRecentDeposits(const DepositsConstraints& deposi Deposits deposits; CurlPostData options; if (depositsConstraints.isCurDefined()) { - options.push_back("asset", depositsConstraints.currencyCode().str()); + options.emplace_back("asset", depositsConstraints.currencyCode().str()); } auto [res, err] = PrivateQuery(_curlHandle, _apiKey, "/private/DepositStatus", options); for (const json& trx : res) { @@ -500,7 +500,7 @@ Withdraw::Status WithdrawStatusFromStatusStr(std::string_view statusStr) { CurlPostData CreateOptionsFromWithdrawConstraints(const WithdrawsConstraints& withdrawsConstraints) { CurlPostData options; if (withdrawsConstraints.isCurDefined()) { - options.push_back("asset", withdrawsConstraints.currencyCode().str()); + options.emplace_back("asset", withdrawsConstraints.currencyCode().str()); } return options; } @@ -587,7 +587,7 @@ PlaceOrderInfo KrakenPrivate::placeOrder([[maybe_unused]] MonetaryAmount from, M {"expiretm", nbSecondsSinceEpoch + expireTimeInSeconds}, {"userref", tradeInfo.tradeContext.userRef}}; if (isSimulation) { - placePostData.push_back("validate", "true"); // validate inputs only. do not submit order (optional) + placePostData.emplace_back("validate", "true"); // validate inputs only. do not submit order (optional) } auto [placeOrderRes, err] = PrivateQuery(_curlHandle, _apiKey, "/private/AddOrder", std::move(placePostData)); diff --git a/src/api/exchanges/src/kucoinprivateapi.cpp b/src/api/exchanges/src/kucoinprivateapi.cpp index 5787bc4e..ca148f00 100644 --- a/src/api/exchanges/src/kucoinprivateapi.cpp +++ b/src/api/exchanges/src/kucoinprivateapi.cpp @@ -240,14 +240,14 @@ void FillOrders(const OrdersConstraints& ordersConstraints, CurlHandle& curlHand exchangePublic.determineMarketFromFilterCurrencies(markets, ordersConstraints.cur1(), ordersConstraints.cur2()); if (filterMarket.isDefined()) { - params.push_back("symbol", filterMarket.assetsPairStrUpper('-')); + params.emplace_back("symbol", filterMarket.assetsPairStrUpper('-')); } } if (ordersConstraints.isPlacedTimeAfterDefined()) { - params.push_back("startAt", TimestampToMillisecondsSinceEpoch(ordersConstraints.placedAfter())); + params.emplace_back("startAt", TimestampToMillisecondsSinceEpoch(ordersConstraints.placedAfter())); } if (ordersConstraints.isPlacedTimeBeforeDefined()) { - params.push_back("endAt", TimestampToMillisecondsSinceEpoch(ordersConstraints.placedBefore())); + params.emplace_back("endAt", TimestampToMillisecondsSinceEpoch(ordersConstraints.placedBefore())); } json data = PrivateQuery(curlHandle, apiKey, HttpRequestType::kGet, "/api/v1/orders", std::move(params))["data"]; @@ -313,7 +313,7 @@ int KucoinPrivate::cancelOpenedOrders(const OrdersConstraints& openedOrdersConst if (openedOrdersConstraints.isMarketOnlyDependent() || openedOrdersConstraints.noConstraints()) { CurlPostData params; if (openedOrdersConstraints.isMarketDefined()) { - params.push_back("symbol", openedOrdersConstraints.market().assetsPairStrUpper('-')); + params.emplace_back("symbol", openedOrdersConstraints.market().assetsPairStrUpper('-')); } json res = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kDelete, "/api/v1/orders", std::move(params)); int nbCancelledOrders = 0; @@ -351,17 +351,17 @@ Deposit::Status DepositStatusFromStatusStr(std::string_view statusStr) { DepositsSet KucoinPrivate::queryRecentDeposits(const DepositsConstraints& depositsConstraints) { CurlPostData options; if (depositsConstraints.isCurDefined()) { - options.push_back("currency", depositsConstraints.currencyCode().str()); + options.emplace_back("currency", depositsConstraints.currencyCode().str()); } if (depositsConstraints.isTimeAfterDefined()) { - options.push_back("startAt", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeAfter())); + options.emplace_back("startAt", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeAfter())); } if (depositsConstraints.isTimeBeforeDefined()) { - options.push_back("endAt", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeBefore())); + options.emplace_back("endAt", TimestampToMillisecondsSinceEpoch(depositsConstraints.timeBefore())); } if (depositsConstraints.isIdDefined()) { if (depositsConstraints.idSet().size() == 1) { - options.push_back("txId", depositsConstraints.idSet().front()); + options.emplace_back("txId", depositsConstraints.idSet().front()); } } json depositJson = @@ -426,13 +426,13 @@ Withdraw::Status WithdrawStatusFromStatusStr(std::string_view statusStr, bool lo CurlPostData CreateOptionsFromWithdrawConstraints(const WithdrawsConstraints& withdrawsConstraints) { CurlPostData options; if (withdrawsConstraints.isCurDefined()) { - options.push_back("currency", withdrawsConstraints.currencyCode().str()); + options.emplace_back("currency", withdrawsConstraints.currencyCode().str()); } if (withdrawsConstraints.isTimeAfterDefined()) { - options.push_back("startAt", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeAfter())); + options.emplace_back("startAt", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeAfter())); } if (withdrawsConstraints.isTimeBeforeDefined()) { - options.push_back("endAt", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeBefore())); + options.emplace_back("endAt", TimestampToMillisecondsSinceEpoch(withdrawsConstraints.timeBefore())); } return options; } @@ -505,19 +505,19 @@ PlaceOrderInfo KucoinPrivate::placeOrder(MonetaryAmount from, MonetaryAmount vol std::string_view strategyType = isTakerStrategy ? "market" : "limit"; CurlPostData params = KucoinPublic::GetSymbolPostData(mk); - params.push_back("clientOid", Nonce_TimeSinceEpochInMs()); - params.push_back("side", buyOrSell); - params.push_back("type", strategyType); - params.push_back("remark", "Placed by coincenter client"); - params.push_back("tradeType", "TRADE"); - params.push_back("size", volume.amountStr()); + params.emplace_back("clientOid", Nonce_TimeSinceEpochInMs()); + params.emplace_back("side", buyOrSell); + params.emplace_back("type", strategyType); + params.emplace_back("remark", "Placed by coincenter client"); + params.emplace_back("tradeType", "TRADE"); + params.emplace_back("size", volume.amountStr()); if (!isTakerStrategy) { - params.push_back("price", price.amountStr()); + params.emplace_back("price", price.amountStr()); } // Add automatic cancelling just in case program unexpectedly stops - params.push_back("timeInForce", "GTT"); // Good until cancelled or time expires - params.push_back("cancelAfter", std::chrono::duration_cast(tradeInfo.options.maxTradeTime()).count() + 1); + params.emplace_back("timeInForce", "GTT"); // Good until cancelled or time expires + params.emplace_back("cancelAfter", std::chrono::duration_cast(tradeInfo.options.maxTradeTime()).count() + 1); json result = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, "/api/v1/orders", std::move(params))["data"]; placeOrderInfo.orderId = std::move(result["orderId"].get_ref()); @@ -576,7 +576,7 @@ InitiatedWithdrawInfo KucoinPrivate::launchWithdraw(MonetaryAmount grossAmount, {"address", destinationWallet.address()}, {"amount", netEmittedAmount.amountStr()}}; if (destinationWallet.hasTag()) { - opts.push_back("memo", destinationWallet.tag()); + opts.emplace_back("memo", destinationWallet.tag()); } json result = diff --git a/src/api/exchanges/src/upbitprivateapi.cpp b/src/api/exchanges/src/upbitprivateapi.cpp index 01559722..382e1f5a 100644 --- a/src/api/exchanges/src/upbitprivateapi.cpp +++ b/src/api/exchanges/src/upbitprivateapi.cpp @@ -266,7 +266,7 @@ void FillOrders(const OrdersConstraints& ordersConstraints, CurlHandle& curlHand exchangePublic.determineMarketFromFilterCurrencies(markets, ordersConstraints.cur1(), ordersConstraints.cur2()); if (filterMarket.isDefined()) { - params.push_back("market", UpbitPublic::ReverseMarketStr(filterMarket)); + params.emplace_back("market", UpbitPublic::ReverseMarketStr(filterMarket)); } } @@ -390,12 +390,12 @@ DepositsSet UpbitPrivate::queryRecentDeposits(const DepositsConstraints& deposit Deposits deposits; CurlPostData options{{"limit", kNbResultsPerPage}}; if (depositsConstraints.isCurDefined()) { - options.push_back("currency", depositsConstraints.currencyCode().str()); + options.emplace_back("currency", depositsConstraints.currencyCode().str()); } if (depositsConstraints.isIdDefined()) { for (std::string_view depositId : depositsConstraints.idSet()) { // Use the "PHP" method of arrays in query string parameter - options.push_back("txids[]", depositId); + options.emplace_back("txids[]", depositId); } } @@ -456,12 +456,12 @@ Withdraw::Status WithdrawStatusFromStatusStr(std::string_view statusStr) { CurlPostData CreateOptionsFromWithdrawConstraints(const WithdrawsConstraints& withdrawsConstraints) { CurlPostData options{{"limit", kNbResultsPerPage}}; if (withdrawsConstraints.isCurDefined()) { - options.push_back("currency", withdrawsConstraints.currencyCode().str()); + options.emplace_back("currency", withdrawsConstraints.currencyCode().str()); } if (withdrawsConstraints.isIdDefined()) { for (std::string_view depositId : withdrawsConstraints.idSet()) { // Use the "PHP" method of arrays in query string parameter - options.push_back("txids[]", depositId); + options.emplace_back("txids[]", depositId); } } return options; @@ -602,13 +602,13 @@ PlaceOrderInfo UpbitPrivate::placeOrder(MonetaryAmount from, MonetaryAmount volu if (isTakerStrategy) { // Upbit has an exotic way to distinguish buy and sell on the same market if (fromCurrencyCode == mk.base()) { - placePostData.push_back("volume", volume.amountStr()); + placePostData.emplace_back("volume", volume.amountStr()); } else { - placePostData.push_back("price", from.amountStr()); + placePostData.emplace_back("price", from.amountStr()); } } else { - placePostData.push_back("volume", volume.amountStr()); - placePostData.push_back("price", price.amountStr()); + placePostData.emplace_back("volume", volume.amountStr()); + placePostData.emplace_back("price", price.amountStr()); } json placeOrderRes = PrivateQuery(_curlHandle, _apiKey, HttpRequestType::kPost, "/v1/orders", placePostData); @@ -663,7 +663,7 @@ InitiatedWithdrawInfo UpbitPrivate::launchWithdraw(MonetaryAmount grossAmount, W {"amount", netEmittedAmount.amountStr()}, {"address", destinationWallet.address()}}; if (destinationWallet.hasTag()) { - withdrawPostData.push_back("secondary_address", destinationWallet.tag()); + withdrawPostData.emplace_back("secondary_address", destinationWallet.tag()); } json result = diff --git a/src/http-request/include/curloptions.hpp b/src/http-request/include/curloptions.hpp index dc342673..9223ce24 100644 --- a/src/http-request/include/curloptions.hpp +++ b/src/http-request/include/curloptions.hpp @@ -53,8 +53,8 @@ class CurlOptions { void clearHttpHeaders() { _httpHeaders.clear(); } - void appendHttpHeader(std::string_view key, std::string_view value) { _httpHeaders.push_back(key, value); } - void appendHttpHeader(std::string_view key, std::integral auto value) { _httpHeaders.push_back(key, value); } + void appendHttpHeader(std::string_view key, std::string_view value) { _httpHeaders.emplace_back(key, value); } + void appendHttpHeader(std::string_view key, std::integral auto value) { _httpHeaders.emplace_back(key, value); } void setHttpHeader(std::string_view key, std::string_view value) { _httpHeaders.set(key, value); } void setHttpHeader(std::string_view key, std::integral auto value) { _httpHeaders.set(key, value); } @@ -64,7 +64,7 @@ class CurlOptions { private: void setPostDataInJsonFormat() { - _httpHeaders.push_back("Content-Type", "application/json"); + _httpHeaders.emplace_back("Content-Type", "application/json"); _postdataInJsonFormat = true; } diff --git a/src/http-request/src/curlhandle.cpp b/src/http-request/src/curlhandle.cpp index 4e4e8a19..72130e5f 100644 --- a/src/http-request/src/curlhandle.cpp +++ b/src/http-request/src/curlhandle.cpp @@ -323,7 +323,7 @@ void CurlHandle::setOverridenQueryResponses(const std::map &quer } FlatQueryResponseMap flatQueryResponses; for (const auto &[query, response] : queryResponsesMap) { - flatQueryResponses.push_back(query, response); + flatQueryResponses.emplace_back(query, response); } _queryData = string(flatQueryResponses.str()); } diff --git a/src/monitoring/src/metric.cpp b/src/monitoring/src/metric.cpp index a8cbc14f..5b948f7c 100644 --- a/src/monitoring/src/metric.cpp +++ b/src/monitoring/src/metric.cpp @@ -6,8 +6,8 @@ namespace cct { MetricKey CreateMetricKey(std::string_view name, std::string_view help) { MetricKey key; - key.push_back(kMetricNameKey, name); - key.push_back(kMetricHelpKey, help); + key.emplace_back(kMetricNameKey, name); + key.emplace_back(kMetricHelpKey, help); return key; } diff --git a/src/tech/CMakeLists.txt b/src/tech/CMakeLists.txt index 0e5909e9..5bb12a8c 100644 --- a/src/tech/CMakeLists.txt +++ b/src/tech/CMakeLists.txt @@ -29,6 +29,11 @@ add_unit_test( test/cct_hash_test.cpp ) +add_unit_test( + char-hexadecimal-converter_test + test/char-hexadecimal-converter_test.cpp +) + add_unit_test( codec_test src/codec.cpp @@ -142,3 +147,8 @@ add_unit_test( DEFINITIONS CCT_DISABLE_SPDLOG ) + +add_unit_test( + url-encode_test.cpp + test/url-encode_test.cpp +) \ No newline at end of file diff --git a/src/tech/include/char-hexadecimal-converter.hpp b/src/tech/include/char-hexadecimal-converter.hpp new file mode 100644 index 00000000..c71de385 --- /dev/null +++ b/src/tech/include/char-hexadecimal-converter.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace cct { + +template +concept signed_or_unsigned_char = std::same_as || std::same_as; + +/// Writes to 'buf' the 2-char hexadecimal code of given char 'ch'. +/// Given buffer should have space for at least two chars. +/// Letters will be in lower case. +/// Return a pointer to the char immediately positioned after the written hexadecimal code. +/// Examples: +/// ',' -> "2c" +/// '?' -> "3f" +constexpr char *to_lower_hex(signed_or_unsigned_char auto ch, char *buf) { + // TODO: can be static in C++23 + constexpr const char *const kHexits = "0123456789abcdef"; + + buf[0] = kHexits[static_cast(ch) >> 4U]; + buf[1] = kHexits[static_cast(ch) & 0x0F]; + + return buf + 2; +} + +/// Writes to 'buf' the 2-char hexadecimal code of given char 'ch'. +/// Given buffer should have space for at least two chars. +/// Letters will be in upper case. +/// Return a pointer to the char immediately positioned after the written hexadecimal code. +/// Examples: +/// ',' -> "2C" +/// '?' -> "3F" +constexpr char *to_upper_hex(signed_or_unsigned_char auto ch, char *buf) { + // TODO: can be static in C++23 + constexpr const char *const kHexits = "0123456789ABCDEF"; + + buf[0] = kHexits[static_cast(ch) >> 4U]; + buf[1] = kHexits[static_cast(ch) & 0x0F]; + + return buf + 2; +} + +} // namespace cct \ No newline at end of file diff --git a/src/tech/include/codec.hpp b/src/tech/include/codec.hpp index aa795955..4051ec72 100644 --- a/src/tech/include/codec.hpp +++ b/src/tech/include/codec.hpp @@ -18,10 +18,4 @@ string B64Encode(const char *) = delete; [[nodiscard]] string B64Decode(std::span ascData); string B64Decode(const char *) = delete; -/// This function converts the given input string to a URL encoded string. -/// All input characters that are not a-z, A-Z, 0-9, '-', '.', '_' or '~' are converted to their "URL escaped" version -/// (%NN where NN is a two-digit hexadecimal number). -[[nodiscard]] string URLEncode(std::span ascData); -string URLEncode(const char *) = delete; - } // namespace cct \ No newline at end of file diff --git a/src/tech/include/flatkeyvaluestring.hpp b/src/tech/include/flatkeyvaluestring.hpp index 1d092c4b..ab048659 100644 --- a/src/tech/include/flatkeyvaluestring.hpp +++ b/src/tech/include/flatkeyvaluestring.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -22,6 +21,7 @@ #include "cct_type_traits.hpp" #include "cct_vector.hpp" #include "unreachable.hpp" +#include "url-encode.hpp" namespace cct { @@ -228,7 +228,7 @@ class FlatKeyValueString { auto front() const { return *begin(); } auto back() const { return *(--end()); } - /// Append a new value for a key. No check is done on a duplicate key. + /// Pushes a new {key, value} entry to the back of the FlatKeyValueString. No check is done on a duplicate key. /// There are several ways to set values as arrays (and none is standard). Choose the method depending on your /// usage: /// - "aKey[]=val1&aKey[]=val2" can be used with several appends (one per value) with the same key suffixed with @@ -240,14 +240,15 @@ class FlatKeyValueString { /// "val": value is a single string /// "val,": value is an array of a single string /// "val1,val2,": value is an array of two values val1 and val2 - void push_back(std::string_view key, std::string_view value); + void emplace_back(std::string_view key, std::string_view value); - void push_back(std::string_view key, std::integral auto val) { + void emplace_back(std::string_view key, std::integral auto val) { // + 1 for minus, +1 for additional partial ranges coverage - char buf[std::numeric_limits::digits10 + 2]; - auto ret = std::to_chars(buf, std::end(buf), val); + std::array::digits10 + 2> buf; - push_back(key, std::string_view(buf, ret.ptr)); + auto [ptr, errc] = std::to_chars(buf.data(), buf.data() + buf.size(), val); + + emplace_back(key, std::string_view(buf.data(), ptr)); } void push_back(const KeyValuePair &kvPair); @@ -256,14 +257,16 @@ class FlatKeyValueString { /// No check is made on duplicated keys. void append(const FlatKeyValueString &rhs); - /// Like push_back, but insert at beginning instead - void push_front(std::string_view key, std::string_view value); + /// Pushes a new {key, value} entry at the front of this buffer. + void emplace_front(std::string_view key, std::string_view value); - void push_front(std::string_view key, std::integral auto i) { + void emplace_front(std::string_view key, std::integral auto val) { // + 1 for minus, +1 for additional partial ranges coverage - char buf[std::numeric_limits::digits10 + 2]; - auto ret = std::to_chars(buf, std::end(buf), i); - push_front(key, std::string_view(buf, ret.ptr)); + std::array::digits10 + 2> buf; + + auto [ptr, errc] = std::to_chars(buf.data(), buf.data() + buf.size(), val); + + emplace_front(key, std::string_view(buf.data(), ptr)); } void push_front(const KeyValuePair &kvPair); @@ -319,7 +322,7 @@ class FlatKeyValueString { /// Returns a new FlatKeyValueString URL encoded except delimiters. FlatKeyValueString urlEncodeExceptDelimiters() const; - auto operator<=>(const FlatKeyValueString &) const = default; + auto operator<=>(const FlatKeyValueString &) const noexcept = default; using trivially_relocatable = is_trivially_relocatable::type; @@ -332,13 +335,11 @@ class FlatKeyValueString { template FlatKeyValueString::FlatKeyValueString(std::span init) { - for (const KeyValuePair &kv : init) { - push_back(kv); - } + std::ranges::for_each(init, [this](const auto &kv) { push_back(kv); }); } template -void FlatKeyValueString::push_back(std::string_view key, std::string_view value) { +void FlatKeyValueString::emplace_back(std::string_view key, std::string_view value) { assert(!key.empty()); assert(!value.empty()); assert(key.find(KeyValuePairSep) == std::string_view::npos); @@ -359,10 +360,10 @@ template inline void FlatKeyValueString::push_back(const KeyValuePair &kv) { switch (kv.val.index()) { case 0: - push_back(kv.key, std::get(kv.val)); + emplace_back(kv.key, std::get(kv.val)); break; case 1: - push_back(kv.key, std::get(kv.val)); + emplace_back(kv.key, std::get(kv.val)); break; default: unreachable(); @@ -380,7 +381,7 @@ void FlatKeyValueString::append(const FlatKeyVa } template -void FlatKeyValueString::push_front(std::string_view key, std::string_view value) { +void FlatKeyValueString::emplace_front(std::string_view key, std::string_view value) { assert(!key.empty()); assert(!value.empty()); assert(key.find(KeyValuePairSep) == std::string_view::npos); @@ -403,10 +404,10 @@ template inline void FlatKeyValueString::push_front(const KeyValuePair &kv) { switch (kv.val.index()) { case 0: - push_front(kv.key, std::get(kv.val)); + emplace_front(kv.key, std::get(kv.val)); break; case 1: - push_front(kv.key, std::get(kv.val)); + emplace_front(kv.key, std::get(kv.val)); break; default: unreachable(); @@ -423,7 +424,7 @@ void FlatKeyValueString::set(std::string_view k const std::size_t pos = find(key); if (pos == string::npos) { - push_back(key, value); + emplace_back(key, value); } else { string::const_iterator first = _data.begin() + pos + key.size() + 1; string::const_iterator last = first + 1; @@ -535,23 +536,10 @@ json FlatKeyValueString::toJson() const { template FlatKeyValueString FlatKeyValueString::urlEncodeExceptDelimiters() const { - string ret(3U * _data.size(), '\0'); - char *outCharIt = ret.data(); - for (char ch : _data) { - if (isalnum(ch) || ch == '@' || ch == '.' || ch == '\\' || ch == '-' || ch == '_' || ch == ':' || - ch == KeyValuePairSep || ch == AssignmentChar) { - *outCharIt++ = ch; - } else { -#ifdef CCT_MSVC - sprintf_s(outCharIt, 4, "%%%02X", static_cast(ch)); -#else - std::sprintf(outCharIt, "%%%02X", static_cast(ch)); -#endif - outCharIt += 3; - } - } - ret.resize(outCharIt - ret.data()); - return FlatKeyValueString(std::move(ret)); + return FlatKeyValueString(URLEncode(_data, [](char ch) { + return isalnum(ch) || ch == '@' || ch == '.' || ch == '\\' || ch == '-' || ch == '_' || ch == ':' || + ch == KeyValuePairSep || ch == AssignmentChar; + })); } } // namespace cct diff --git a/src/tech/include/stringhelpers.hpp b/src/tech/include/stringhelpers.hpp index 7aea2712..544682b7 100644 --- a/src/tech/include/stringhelpers.hpp +++ b/src/tech/include/stringhelpers.hpp @@ -15,8 +15,8 @@ namespace cct { namespace details { template -void ToChars(char *first, SizeType s, std::integral auto i) { - if (auto [ptr, errc] = std::to_chars(first, first + s, i); CCT_UNLIKELY(errc != std::errc())) { +void ToChars(char *first, SizeType sz, std::integral auto val) { + if (auto [ptr, errc] = std::to_chars(first, first + sz, val); CCT_UNLIKELY(errc != std::errc())) { throw exception("Unable to decode integral into string"); } } @@ -24,9 +24,9 @@ void ToChars(char *first, SizeType s, std::integral auto i) { inline string ToString(std::integral auto val) { const int nbDigitsInt = nchars(val); - string s(nbDigitsInt, '0'); - details::ToChars(s.data(), nbDigitsInt, val); - return s; + string str(nbDigitsInt, '0'); + details::ToChars(str.data(), nbDigitsInt, val); + return str; } template @@ -42,17 +42,17 @@ Integral FromString(std::string_view str) { } template -void SetString(StringType &s, std::integral auto i) { - const int nbDigitsInt = nchars(i); - s.resize(nbDigitsInt, '0'); - details::ToChars(s.data(), nbDigitsInt, i); +void SetString(StringType &str, std::integral auto val) { + const int nbDigitsInt = nchars(val); + str.resize(nbDigitsInt, '0'); + details::ToChars(str.data(), nbDigitsInt, val); } template -void AppendString(StringType &s, std::integral auto i) { - const int nbDigitsInt = nchars(i); - s.append(nbDigitsInt, '0'); - details::ToChars(s.data() + static_cast(s.size()) - nbDigitsInt, nbDigitsInt, i); +void AppendString(StringType &str, std::integral auto val) { + const int nbDigitsInt = nchars(val); + str.append(nbDigitsInt, '0'); + details::ToChars(str.data() + static_cast(str.size()) - nbDigitsInt, nbDigitsInt, val); } inline std::size_t strnlen(const char *start, std::size_t maxLen) { diff --git a/src/tech/include/url-encode.hpp b/src/tech/include/url-encode.hpp new file mode 100644 index 00000000..e23fc261 --- /dev/null +++ b/src/tech/include/url-encode.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +#include "cct_string.hpp" +#include "char-hexadecimal-converter.hpp" + +namespace cct { + +/// This function converts the given input string to a URL encoded string. +/// All input characters 'ch' for which isNotEncodedFunc(ch) is false are converted in upper case hexadecimal. +/// (%NN where NN is a two-digit hexadecimal number). +template +string URLEncode(std::span data, IsNotEncodedFunc isNotEncodedFunc) { + const auto nbNotEncodedChars = std::ranges::count_if(data, isNotEncodedFunc); + const auto nbEncodedChars = data.size() - nbNotEncodedChars; + + string ret(nbNotEncodedChars + 3U * nbEncodedChars, '\0'); + + char* outCharIt = ret.data(); + for (char ch : data) { + if (isNotEncodedFunc(ch)) { + *outCharIt++ = ch; + } else { + *outCharIt++ = '%'; + outCharIt = to_upper_hex(ch, outCharIt); + } + } + return ret; +} + +// const char * argument is deleted because it would construct into a span including the unwanted null +// terminating character. Use span directly, or string / string_view instead. + +template +string URLEncode(const char*, IsNotEncodedFunc) = delete; + +} // namespace cct diff --git a/src/tech/src/codec.cpp b/src/tech/src/codec.cpp index a504fb4b..75216204 100644 --- a/src/tech/src/codec.cpp +++ b/src/tech/src/codec.cpp @@ -1,23 +1,22 @@ #include "codec.hpp" #include -#include #include #include "cct_cctype.hpp" #include "cct_invalid_argument_exception.hpp" #include "cct_string.hpp" +#include "char-hexadecimal-converter.hpp" namespace cct { string BinToHex(std::span binData) { - static constexpr const char* const kHexits = "0123456789abcdef"; - auto sz = binData.size(); - string ret(2 * sz, '\0'); - while (sz != 0) { - --sz; - ret[2 * sz] = kHexits[binData[sz] >> 4]; - ret[(2 * sz) + 1] = kHexits[binData[sz] & 0x0F]; + string ret(2 * binData.size(), '\0'); + + auto out = ret.data(); + + for (auto beg = binData.data(), end = beg + binData.size(); beg != end; ++beg) { + out = to_lower_hex(*beg, out); } return ret; } @@ -77,22 +76,4 @@ string B64Decode(std::span ascData) { return ret; } -string URLEncode(std::span ascData) { - string ret(3U * ascData.size(), '\0'); - char* outCharIt = ret.data(); - for (char ch : ascData) { - if (isalnum(ch) || ch == '-' || ch == '.' || ch == '_' || ch == '~') { - *outCharIt++ = ch; - } else { -#ifdef CCT_MSVC - sprintf_s(outCharIt, 4, "%%%02X", static_cast(ch)); -#else - std::sprintf(outCharIt, "%%%02X", static_cast(ch)); -#endif - outCharIt += 3; - } - } - ret.resize(outCharIt - ret.data()); - return ret; -} } // namespace cct diff --git a/src/tech/test/char-hexadecimal-converter_test.cpp b/src/tech/test/char-hexadecimal-converter_test.cpp new file mode 100644 index 00000000..6b66b321 --- /dev/null +++ b/src/tech/test/char-hexadecimal-converter_test.cpp @@ -0,0 +1,55 @@ +#include "char-hexadecimal-converter.hpp" + +#include + +#include + +namespace cct { + +class CharHexadecimalConverterTest : public ::testing::Test { + protected: + constexpr std::string_view ToUpperHex(char ch) { return {buf, to_upper_hex(ch, buf)}; } + + constexpr std::string_view ToLowerHex(char ch) { return {buf, to_lower_hex(ch, buf)}; } + + char buf[2]; +}; + +TEST_F(CharHexadecimalConverterTest, StandardAscii) { + EXPECT_EQ(ToUpperHex(0), "00"); + EXPECT_EQ(ToUpperHex(1), "01"); + EXPECT_EQ(ToUpperHex(2), "02"); + + EXPECT_EQ(ToUpperHex(11), "0B"); + + EXPECT_EQ(ToUpperHex(','), "2C"); + EXPECT_EQ(ToUpperHex('?'), "3F"); + + EXPECT_EQ(ToUpperHex('^'), "5E"); + + EXPECT_EQ(ToUpperHex('a'), "61"); + + EXPECT_EQ(ToUpperHex(127), "7F"); +} + +TEST_F(CharHexadecimalConverterTest, ExtendedAscii) { + EXPECT_EQ(ToUpperHex(static_cast(128)), "80"); + EXPECT_EQ(ToUpperHex(static_cast(129)), "81"); + + EXPECT_EQ(ToUpperHex(static_cast(155)), "9B"); + EXPECT_EQ(ToUpperHex(static_cast(169)), "A9"); + + EXPECT_EQ(ToUpperHex(static_cast(255)), "FF"); +} + +TEST_F(CharHexadecimalConverterTest, ToLowerHex) { + EXPECT_EQ(ToLowerHex(static_cast(128)), "80"); + EXPECT_EQ(ToLowerHex(static_cast(129)), "81"); + + EXPECT_EQ(ToLowerHex(static_cast(155)), "9b"); + EXPECT_EQ(ToLowerHex(static_cast(169)), "a9"); + + EXPECT_EQ(ToLowerHex(static_cast(255)), "ff"); +} + +} // namespace cct \ No newline at end of file diff --git a/src/tech/test/codec_test.cpp b/src/tech/test/codec_test.cpp index 3e1b2fc3..8499cfb1 100644 --- a/src/tech/test/codec_test.cpp +++ b/src/tech/test/codec_test.cpp @@ -46,9 +46,4 @@ TEST(Base64, Decode7) { EXPECT_EQ(B64Decode(std::string_view("Zm9vYmFyeg==")), " TEST(Base64, Decode8) { EXPECT_EQ(B64Decode(std::string_view("Zm9vYmFyelk=")), "foobarzY"); } TEST(Base64, Decode9) { EXPECT_EQ(B64Decode(std::string_view("Zm9vYmFyelln")), "foobarzYg"); } -TEST(URLEncode, Test1) { - EXPECT_EQ(URLEncode(std::string_view("2023-05-14T09:42:00")), "2023-05-14T09%3A42%3A00"); - EXPECT_EQ(URLEncode(std::string_view("GORgnlPotuGH5cv4JK8d63JWQqkyCPyIo/Z09DvPd4g=")), - "GORgnlPotuGH5cv4JK8d63JWQqkyCPyIo%2FZ09DvPd4g%3D"); -} } // namespace cct diff --git a/src/tech/test/flatkeyvaluestring_test.cpp b/src/tech/test/flatkeyvaluestring_test.cpp index 24e0db38..3f2df653 100644 --- a/src/tech/test/flatkeyvaluestring_test.cpp +++ b/src/tech/test/flatkeyvaluestring_test.cpp @@ -33,7 +33,7 @@ TEST(FlatKeyValueStringTest, SetEmpty) { TEST(FlatKeyValueStringTest, SetAndAppend) { KvPairs kvPairs; - kvPairs.push_back("abc", "666"); + kvPairs.emplace_back("abc", "666"); kvPairs.push_back({"de", "aX"}); EXPECT_EQ(kvPairs.get("def"), ""); EXPECT_FALSE(kvPairs.empty()); @@ -53,11 +53,11 @@ TEST(FlatKeyValueStringTest, SetAndAppend) { EXPECT_EQ(kvPairs.str(), "abc=777&de=aX&def=titi&777=yoplalepiege&d=encoreplustricky"); kvPairs.set("d", "cestboncestfini"); EXPECT_EQ(kvPairs.str(), "abc=777&de=aX&def=titi&777=yoplalepiege&d=cestboncestfini"); - kvPairs.push_back("newKey", "="); + kvPairs.emplace_back("newKey", "="); EXPECT_EQ(kvPairs.str(), "abc=777&de=aX&def=titi&777=yoplalepiege&d=cestboncestfini&newKey=="); - kvPairs.push_back("$5*(%", ".9h===,Mj"); + kvPairs.emplace_back("$5*(%", ".9h===,Mj"); EXPECT_EQ(kvPairs.str(), "abc=777&de=aX&def=titi&777=yoplalepiege&d=cestboncestfini&newKey==&$5*(%=.9h===,Mj"); - kvPairs.push_back("encoreplustricky", "="); + kvPairs.emplace_back("encoreplustricky", "="); EXPECT_EQ(kvPairs.str(), "abc=777&de=aX&def=titi&777=yoplalepiege&d=cestboncestfini&newKey==&$5*(%=.9h===,Mj&encoreplustricky=="); kvPairs.set("$5*(%", ".9h==,Mj"); @@ -67,13 +67,13 @@ TEST(FlatKeyValueStringTest, SetAndAppend) { TEST(FlatKeyValueStringTest, Prepend) { KvPairs kvPairs; - kvPairs.push_front("statue", "liberty"); + kvPairs.emplace_front("statue", "liberty"); EXPECT_EQ(kvPairs.str(), "statue=liberty"); - kvPairs.push_front("city", "New York City"); + kvPairs.emplace_front("city", "New York City"); EXPECT_EQ(kvPairs.str(), "city=New York City&statue=liberty"); kvPairs.push_front({"state", "New York"}); EXPECT_EQ(kvPairs.str(), "state=New York&city=New York City&statue=liberty"); - kvPairs.push_front("Postal Code", 10015); + kvPairs.emplace_front("Postal Code", 10015); EXPECT_EQ(kvPairs.str(), "Postal Code=10015&state=New York&city=New York City&statue=liberty"); } @@ -105,7 +105,7 @@ TEST(FlatKeyValueStringTest, WithNullTerminatingCharAsSeparator) { EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0rm:Yy3\0huhu:haha"sv)); kvPairs.erase("rm"); EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0huhu:haha"sv)); - kvPairs.push_back("&newField", "&&newValue&&"); + kvPairs.emplace_back("&newField", "&&newValue&&"); EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0huhu:haha\0&newField:&&newValue&&"sv)); int kvPairPos = 0; @@ -131,13 +131,13 @@ TEST(FlatKeyValueStringTest, WithNullTerminatingCharAsSeparator) { TEST(FlatKeyValueStringTest, EmptyConvertToJson) { EXPECT_EQ(KvPairs().toJson(), json()); } -class CurlOptionsCase1 : public ::testing::Test { +class FlatKeyValueStringCase1 : public ::testing::Test { protected: KvPairs kvPairs{{"units", "0.11176"}, {"price", "357.78"}, {"777", "encoredutravail?"}, {"hola", "quetal"}, {"k", "v"}, {"array1", "val1,,"}, {"array2", ",val1,val2,value,"}, {"emptyArray", ","}}; }; -TEST_F(CurlOptionsCase1, Front) { +TEST_F(FlatKeyValueStringCase1, Front) { const auto &kvFront = kvPairs.front(); EXPECT_EQ(kvFront.key(), "units"); @@ -149,7 +149,7 @@ TEST_F(CurlOptionsCase1, Front) { EXPECT_EQ(kvFront.size(), 13U); } -TEST_F(CurlOptionsCase1, Back) { +TEST_F(FlatKeyValueStringCase1, Back) { const auto kvFront = kvPairs.back(); EXPECT_EQ(kvFront.key(), "emptyArray"); @@ -161,7 +161,7 @@ TEST_F(CurlOptionsCase1, Back) { EXPECT_EQ(kvFront.size(), 12U); } -TEST_F(CurlOptionsCase1, PopBack) { +TEST_F(FlatKeyValueStringCase1, PopBack) { EXPECT_NE(kvPairs.find("emptyArray"), string::npos); kvPairs.pop_back(); EXPECT_EQ(kvPairs.find("emptyArray"), string::npos); @@ -171,7 +171,7 @@ TEST_F(CurlOptionsCase1, PopBack) { EXPECT_EQ(newBack.val(), ",val1,val2,value,"); } -TEST_F(CurlOptionsCase1, Get) { +TEST_F(FlatKeyValueStringCase1, Get) { EXPECT_EQ(kvPairs.get("units"), "0.11176"); EXPECT_EQ(kvPairs.get("price"), "357.78"); EXPECT_EQ(kvPairs.get("777"), "encoredutravail?"); @@ -184,7 +184,7 @@ TEST_F(CurlOptionsCase1, Get) { EXPECT_EQ(kvPairs.get("laipas"), ""); } -TEST_F(CurlOptionsCase1, ForwardIterator) { +TEST_F(FlatKeyValueStringCase1, ForwardIterator) { int itPos = 0; for (const auto &kv : kvPairs) { const auto key = kv.key(); @@ -229,7 +229,7 @@ TEST_F(CurlOptionsCase1, ForwardIterator) { EXPECT_EQ(itPos, 8); } -TEST_F(CurlOptionsCase1, BackwardIterator) { +TEST_F(FlatKeyValueStringCase1, BackwardIterator) { int itPos = 0; for (auto it = kvPairs.end(); it != kvPairs.begin();) { const auto kv = *--it; @@ -275,7 +275,7 @@ TEST_F(CurlOptionsCase1, BackwardIterator) { EXPECT_EQ(itPos, 8); } -TEST_F(CurlOptionsCase1, EraseIncrementDecrement) { +TEST_F(FlatKeyValueStringCase1, EraseIncrementDecrement) { kvPairs.erase(kvPairs.begin()); const auto &kvFront = kvPairs.front(); @@ -332,7 +332,7 @@ TEST_F(CurlOptionsCase1, EraseIncrementDecrement) { EXPECT_EQ(itPos, 5); } -TEST_F(CurlOptionsCase1, ConvertToJson) { +TEST_F(FlatKeyValueStringCase1, ConvertToJson) { json jsonData = kvPairs.toJson(); EXPECT_EQ(jsonData["units"].get(), "0.11176"); @@ -365,15 +365,15 @@ TEST_F(CurlOptionsCase1, ConvertToJson) { EXPECT_TRUE(arrayIt->empty()); } -TEST_F(CurlOptionsCase1, AppendIntegralValues) { - kvPairs.push_back("price1", 1957386078376L); +TEST_F(FlatKeyValueStringCase1, AppendIntegralValues) { + kvPairs.emplace_back("price1", 1957386078376L); EXPECT_EQ(kvPairs.get("price1"), "1957386078376"); int8_t val = -116; - kvPairs.push_back("testu", val); + kvPairs.emplace_back("testu", val); EXPECT_EQ(kvPairs.get("testu"), "-116"); } -TEST_F(CurlOptionsCase1, SetIntegralValues) { +TEST_F(FlatKeyValueStringCase1, SetIntegralValues) { kvPairs.set("price1", 42); EXPECT_EQ(kvPairs.get("price"), "357.78"); EXPECT_EQ(kvPairs.get("price1"), "42"); @@ -387,4 +387,10 @@ TEST_F(CurlOptionsCase1, SetIntegralValues) { EXPECT_EQ(kvPairs.get("testu"), "-116"); } +TEST_F(FlatKeyValueStringCase1, UrlEncode) { + EXPECT_EQ(kvPairs.urlEncodeExceptDelimiters().str(), + "units=0.11176&price=357.78&777=encoredutravail%3F&hola=quetal&k=v&array1=val1%2C%2C&array2=%2Cval1%2Cval2%" + "2Cvalue%2C&emptyArray=%2C"); +} + } // namespace cct diff --git a/src/tech/test/url-encode_test.cpp b/src/tech/test/url-encode_test.cpp new file mode 100644 index 00000000..e5521a91 --- /dev/null +++ b/src/tech/test/url-encode_test.cpp @@ -0,0 +1,17 @@ +#include "url-encode.hpp" + +#include + +#include "cct_cctype.hpp" + +namespace cct { + +TEST(URLEncode, Test1) { + auto isNotEncoded = [](char ch) { return isalnum(ch) || ch == '-' || ch == '.' || ch == '_' || ch == '~'; }; + + EXPECT_EQ(URLEncode(std::string_view("2023-05-14T09:42:00"), isNotEncoded), "2023-05-14T09%3A42%3A00"); + EXPECT_EQ(URLEncode(std::string_view("GORgnlPotuGH5cv4JK8d63JWQqkyCPyIo/Z09DvPd4g="), isNotEncoded), + "GORgnlPotuGH5cv4JK8d63JWQqkyCPyIo%2FZ09DvPd4g%3D"); +} + +} // namespace cct \ No newline at end of file