diff --git a/include/xrpl/basics/StringUtilities.h b/include/xrpl/basics/StringUtilities.h index 2b360d2fda..d606613c65 100644 --- a/include/xrpl/basics/StringUtilities.h +++ b/include/xrpl/basics/StringUtilities.h @@ -125,9 +125,31 @@ struct ParsedUrl bool parseUrl(ParsedUrl& pUrl, std::string const& strUrl); +/** + * Remove leading and trailing ASCII whitespace. + * + * Whitespace is the fixed set " \t\n\v\f\r"; the current locale is not + * consulted, so the result depends only on the input. + * + * @param str The string to trim. + * @return @p str without leading or trailing whitespace. + */ std::string trimWhitespace(std::string str); +/** + * Fold ASCII upper case letters to lower case. + * + * Only 'A' through 'Z' are remapped; every other byte is left alone and the + * current locale is not consulted, so the result depends only on the input. + * + * @param str The string to fold. + * @return @p str with each ASCII upper case letter replaced by its lower case + * equivalent. + */ +std::string +toLower(std::string str); + std::optional toUInt64(std::string const& s); diff --git a/src/libxrpl/basics/StringUtilities.cpp b/src/libxrpl/basics/StringUtilities.cpp index 2b7deecb8e..9eb1bff995 100644 --- a/src/libxrpl/basics/StringUtilities.cpp +++ b/src/libxrpl/basics/StringUtilities.cpp @@ -5,15 +5,15 @@ #include #include -#include -#include #include #include #include +#include #include #include #include +#include #include #include @@ -67,7 +67,7 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) } pUrl.scheme = smMatch[1]; - boost::algorithm::to_lower(pUrl.scheme); + pUrl.scheme = toLower(pUrl.scheme); pUrl.username = smMatch[2]; pUrl.password = smMatch[3]; std::string const domain = smMatch[4]; @@ -93,10 +93,42 @@ parseUrl(ParsedUrl& pUrl, std::string const& strUrl) return true; } +namespace { + +// Deliberately not std::isspace / std::tolower: those consult the current C +// locale, so the same input could trim or fold differently depending on +// process-wide state set by something else entirely. Everything these helpers +// are used on (config keys and values, URL schemes, hex digests) is ASCII, and +// the callers want a fixed answer, so spell the ASCII rules out. + +constexpr bool +isAsciiSpace(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r'; +} + +constexpr char +toAsciiLower(char c) +{ + return (c >= 'A' && c <= 'Z') ? static_cast(c - 'A' + 'a') : c; +} + +} // namespace + std::string trimWhitespace(std::string str) { - boost::trim(str); + auto const end = std::ranges::find_if_not(str | std::views::reverse, isAsciiSpace).base(); + str.erase(end, str.end()); + str.erase(str.begin(), std::ranges::find_if_not(str, isAsciiSpace)); + + return str; +} + +std::string +toLower(std::string str) +{ + std::ranges::transform(str, str.begin(), toAsciiLower); return str; } diff --git a/src/libxrpl/crypto/RFC1751.cpp b/src/libxrpl/crypto/RFC1751.cpp index 4b17e1443c..f6342928ab 100644 --- a/src/libxrpl/crypto/RFC1751.cpp +++ b/src/libxrpl/crypto/RFC1751.cpp @@ -1,11 +1,11 @@ #include +#include #include #include #include #include -#include #include #include @@ -397,7 +397,7 @@ RFC1751::getKeyFromEnglish(std::string& strKey, std::string const& strHuman) std::string strTrimmed(strHuman); - boost::algorithm::trim(strTrimmed); + strTrimmed = trimWhitespace(strTrimmed); boost::algorithm::split( vWords, strTrimmed, boost::algorithm::is_space(), boost::algorithm::token_compress_on); diff --git a/src/libxrpl/server/Manifest.cpp b/src/libxrpl/server/Manifest.cpp index 0760196a3b..c85c8445f0 100644 --- a/src/libxrpl/server/Manifest.cpp +++ b/src/libxrpl/server/Manifest.cpp @@ -23,8 +23,6 @@ #include #include -#include - #include #include #include @@ -277,7 +275,7 @@ loadValidatorToken(std::vector const& blob, beast::Journal journal) [](std::size_t init, std::string const& s) { return init + s.size(); })); for (auto const& line : blob) - tokenStr += boost::algorithm::trim_copy(line); + tokenStr += trimWhitespace(line); tokenStr = base64Decode(tokenStr); @@ -653,7 +651,7 @@ ManifestCache::load( [](std::size_t init, std::string const& s) { return init + s.size(); })); for (auto const& line : configRevocation) - revocationStr += boost::algorithm::trim_copy(line); + revocationStr += trimWhitespace(line); auto mo = deserializeManifest(base64Decode(revocationStr)); diff --git a/src/libxrpl/server/Port.cpp b/src/libxrpl/server/Port.cpp index 694d4448d5..a7892bc0e8 100644 --- a/src/libxrpl/server/Port.cpp +++ b/src/libxrpl/server/Port.cpp @@ -1,5 +1,6 @@ #include +#include #include #include #include @@ -9,7 +10,6 @@ #include #include -#include #include #include #include @@ -98,7 +98,7 @@ populate( while (std::getline(ss, ip, ',')) { - boost::algorithm::trim(ip); + ip = trimWhitespace(ip); bool v4 = false; boost::asio::ip::network_v4 v4Net; boost::asio::ip::network_v6 v6Net; diff --git a/src/tests/libxrpl/basics/StringUtilities.cpp b/src/tests/libxrpl/basics/StringUtilities.cpp index a10711abdb..0180e25db0 100644 --- a/src/tests/libxrpl/basics/StringUtilities.cpp +++ b/src/tests/libxrpl/basics/StringUtilities.cpp @@ -290,4 +290,44 @@ TEST_F(StringUtilitiesTest, to_string) EXPECT_EQ(result, "hello"); } +TEST_F(StringUtilitiesTest, trimWhitespace) +{ + EXPECT_EQ(trimWhitespace(""), ""); + EXPECT_EQ(trimWhitespace(" "), ""); + EXPECT_EQ(trimWhitespace("abc"), "abc"); + EXPECT_EQ(trimWhitespace(" abc"), "abc"); + EXPECT_EQ(trimWhitespace("abc "), "abc"); + EXPECT_EQ(trimWhitespace(" \t\n\v\f\r abc \t\n\v\f\r "), "abc"); + + // Interior whitespace is preserved. + EXPECT_EQ(trimWhitespace(" a b\tc "), "a b\tc"); +} + +TEST_F(StringUtilitiesTest, toLower) +{ + EXPECT_EQ(toLower(""), ""); + EXPECT_EQ(toLower("ABC"), "abc"); + EXPECT_EQ(toLower("AbC123"), "abc123"); + EXPECT_EQ(toLower("already lower"), "already lower"); + + // Only 'A'-'Z' are remapped. Neighbouring punctuation and digits, which a + // buggy range check could catch, must survive untouched. + EXPECT_EQ(toLower("@[`{_^"), "@[`{_^"); +} + +// Both helpers are documented as depending only on their input. Guard that by +// checking the bytes just outside ASCII, which a locale-aware isspace/tolower +// could classify differently. +TEST_F(StringUtilitiesTest, trimAndLowerIgnoreLocale) +{ + // 0xA0 is NO-BREAK SPACE in Latin-1 and is whitespace to some locales. + std::string const nbsp("\xA0", 1); + EXPECT_EQ(trimWhitespace(nbsp), nbsp); + EXPECT_EQ(trimWhitespace(" " + nbsp + " "), nbsp); + + // 0xC0 is LATIN CAPITAL LETTER A WITH GRAVE in Latin-1. + std::string const agrave("\xC0", 1); + EXPECT_EQ(toLower(agrave), agrave); +} + } // namespace xrpl diff --git a/src/xrpld/app/main/GRPCServer.cpp b/src/xrpld/app/main/GRPCServer.cpp index 1b20ff1d49..fc4a9794bd 100644 --- a/src/xrpld/app/main/GRPCServer.cpp +++ b/src/xrpld/app/main/GRPCServer.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -24,7 +25,6 @@ #include #include -#include #include #include #include @@ -371,7 +371,7 @@ GRPCServerImpl::GRPCServerImpl(Application& app) std::string ip; while (std::getline(ss, ip, ',')) { - boost::algorithm::trim(ip); + ip = trimWhitespace(ip); auto const addr = boost::asio::ip::make_address(ip); if (addr.is_unspecified()) diff --git a/src/xrpld/app/main/Main.cpp b/src/xrpld/app/main/Main.cpp index a23b84f2e8..ba6520db5f 100644 --- a/src/xrpld/app/main/Main.cpp +++ b/src/xrpld/app/main/Main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -21,7 +22,6 @@ #include #include -#include #include #include // IWYU pragma: keep #include @@ -211,7 +211,7 @@ public: boost::split(v, patterns, boost::algorithm::is_any_of(",")); selectors_.reserve(v.size()); std::ranges::for_each(v, [this](std::string s) { - boost::trim(s); + s = trimWhitespace(s); if (selectors_.empty() || !s.empty()) selectors_.emplace_back(beast::unit_test::Selector::ModeT::Automatch, s); }); @@ -614,7 +614,7 @@ run(int argc, char** argv) std::vector result; for (auto& s : strVec) { - boost::trim(s); + s = trimWhitespace(s); if (!s.empty()) result.push_back(std::stoi(s)); } diff --git a/src/xrpld/core/detail/Config.cpp b/src/xrpld/core/detail/Config.cpp index e93ccec56e..f263fb49ab 100644 --- a/src/xrpld/core/detail/Config.cpp +++ b/src/xrpld/core/detail/Config.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -185,7 +184,7 @@ parseIniFile(std::string const& strInput, bool const bTrim) for (auto& strValue : vLines) { if (bTrim) - boost::algorithm::trim(strValue); + strValue = trimWhitespace(strValue); if (strValue.empty() || strValue[0] == '#') { diff --git a/src/xrpld/rpc/detail/ServerHandler.cpp b/src/xrpld/rpc/detail/ServerHandler.cpp index 0181d5b10f..827d8705fd 100644 --- a/src/xrpld/rpc/detail/ServerHandler.cpp +++ b/src/xrpld/rpc/detail/ServerHandler.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -44,7 +45,6 @@ #include #include -#include #include #include #include @@ -113,7 +113,7 @@ authorized(Port const& port, std::map const& h) if ((it == h.end()) || (!it->second.starts_with("Basic "))) return false; std::string strUserPass64 = it->second.substr(6); - boost::trim(strUserPass64); + strUserPass64 = trimWhitespace(strUserPass64); std::string const strUserPass = base64Decode(strUserPass64); std::string::size_type const nColon = strUserPass.find(':'); if (nColon == std::string::npos) diff --git a/src/xrpld/rpc/handlers/account/AccountInfo.cpp b/src/xrpld/rpc/handlers/account/AccountInfo.cpp index f131af01e5..eed4e4cfe3 100644 --- a/src/xrpld/rpc/handlers/account/AccountInfo.cpp +++ b/src/xrpld/rpc/handlers/account/AccountInfo.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -22,7 +23,6 @@ #include #include -#include #include #include @@ -57,7 +57,7 @@ injectSLE(json::Value& jv, SLE const& sle) auto const& hash = sle.getFieldH128(sfEmailHash); Blob const b(hash.begin(), hash.end()); std::string md5 = strHex(makeSlice(b)); - boost::to_lower(md5); + md5 = toLower(md5); // VFALCO TODO Give a name to this constant and move it // to a more visible location. jv[jss::urlgravatar] = str(boost::format("https://www.gravatar.com/avatar/%s") % md5); diff --git a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp index 91db16bb4f..5c96bfb215 100644 --- a/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp +++ b/src/xrpld/rpc/handlers/admin/data/CanDelete.cpp @@ -3,14 +3,13 @@ #include #include +#include #include #include #include #include #include -#include - #include #include #include @@ -38,7 +37,7 @@ doCanDelete(rpc::JsonContext& context) else { std::string canDeleteStr = canDelete.asString(); - boost::to_lower(canDeleteStr); + canDeleteStr = toLower(canDeleteStr); if (canDeleteStr.find_first_not_of("0123456789") == std::string::npos) { diff --git a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp index 32a084a833..c297c2482d 100644 --- a/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp +++ b/src/xrpld/rpc/handlers/server_info/ServerDefinitions.cpp @@ -2,6 +2,7 @@ #include +#include #include #include #include @@ -14,7 +15,6 @@ #include #include -#include #include #include @@ -106,7 +106,7 @@ ServerDefinitions::translate(std::string const& inp) std::string token = inpToProcess.substr(0, pos); if (token.size() > 1) { - boost::algorithm::to_lower(token); + token = toLower(token); token[0] -= ('a' - 'A'); out += token; }