diff --git a/src/opds_dumper.cpp b/src/opds_dumper.cpp index fc1f01c1b..bea9ab9df 100644 --- a/src/opds_dumper.cpp +++ b/src/opds_dumper.cpp @@ -82,7 +82,7 @@ std::string fullEntryXML(const Book& book, const std::string& rootLocation, cons {"title", book.getTitle()}, {"description", book.getDescription()}, {"language", book.getLanguage()}, - {"content_id", urlEncode(contentId, true)}, + {"content_id", urlEncode(contentId)}, {"updated", bookDate}, // XXX: this should be the entry update datetime {"book_date", bookDate}, {"category", book.getCategory()}, @@ -216,7 +216,7 @@ string OPDSDumper::dumpOPDSFeedV2(const std::vector& bookIds, const {"endpoint_root", endpointRoot}, {"feed_id", gen_uuid(libraryId + endpoint + "?" + query)}, {"filter", onlyAsNonEmptyMustacheValue(query)}, - {"query", query.empty() ? "" : "?" + urlEncode(query)}, + {"query", query.empty() ? "" : "?" + query}, {"totalResults", to_string(m_totalResults)}, {"startIndex", to_string(m_startIndex)}, {"itemsPerPage", to_string(m_count)}, diff --git a/src/search_renderer.cpp b/src/search_renderer.cpp index 7b293f628..c47b93368 100644 --- a/src/search_renderer.cpp +++ b/src/search_renderer.cpp @@ -94,7 +94,7 @@ kainjow::mustache::data buildQueryData kainjow::mustache::data query; query.set("pattern", kiwix::encodeDiples(pattern)); std::ostringstream ss; - ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern, true); + ss << searchProtocolPrefix << "?pattern=" << urlEncode(pattern); ss << "&" << bookQuery; query.set("unpaginatedQuery", ss.str()); auto lang = extractValueFromQuery(bookQuery, "books.filter.lang"); @@ -171,9 +171,10 @@ std::string SearchRenderer::renderTemplate(const std::string& tmpl_str) kainjow::mustache::data items{kainjow::mustache::data::type::list}; for (auto it = m_srs.begin(); it != m_srs.end(); it++) { kainjow::mustache::data result; - std::string zim_id(it.getZimId()); + const std::string zim_id(it.getZimId()); + const auto path = mp_nameMapper->getNameForId(zim_id) + "/" + it.getPath(); result.set("title", it.getTitle()); - result.set("absolutePath", absPathPrefix + urlEncode(mp_nameMapper->getNameForId(zim_id), true) + "/" + urlEncode(it.getPath())); + result.set("absolutePath", absPathPrefix + urlEncode(path)); result.set("snippet", it.getSnippet()); if (mp_library) { result.set("bookTitle", mp_library->getBookById(zim_id).getTitle()); diff --git a/src/server/internalServer.cpp b/src/server/internalServer.cpp index 0edb367fc..8548d4245 100644 --- a/src/server/internalServer.cpp +++ b/src/server/internalServer.cpp @@ -1030,7 +1030,7 @@ ParameterizedMessage suggestSearchMsg(const std::string& searchURL, const std::s std::unique_ptr InternalServer::build_redirect(const std::string& bookName, const zim::Item& item) const { - const auto path = kiwix::urlEncode(item.getPath(), true); + const auto path = kiwix::urlEncode(item.getPath()); const auto redirectUrl = m_root + "/content/" + bookName + "/" + path; return Response::build_redirect(*this, redirectUrl); } @@ -1055,7 +1055,7 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r } catch (const std::out_of_range& e) {} if (archive == nullptr) { - const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern, true); + const std::string searchURL = m_root + "/search?pattern=" + kiwix::urlEncode(pattern); return HTTP404Response(*this, request) + urlNotFoundMsg + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern)); @@ -1096,7 +1096,7 @@ std::unique_ptr InternalServer::handle_content(const RequestContext& r if (m_verbose.load()) printf("Failed to find %s\n", urlStr.c_str()); - std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern, true); + std::string searchURL = m_root + "/search?content=" + bookName + "&pattern=" + kiwix::urlEncode(pattern); return HTTP404Response(*this, request) + urlNotFoundMsg + suggestSearchMsg(searchURL, kiwix::urlDecode(pattern)); diff --git a/src/server/request_context.cpp b/src/server/request_context.cpp index 272c7f737..d418d75af 100644 --- a/src/server/request_context.cpp +++ b/src/server/request_context.cpp @@ -116,10 +116,10 @@ MHD_Result RequestContext::fill_argument(void *__this, enum MHD_ValueKind kind, if ( ! _this->queryString.empty() ) { _this->queryString += "&"; } - _this->queryString += key; + _this->queryString += urlEncode(key); if ( value ) { _this->queryString += "="; - _this->queryString += value; + _this->queryString += urlEncode(value); } return MHD_YES; } diff --git a/src/server/request_context.h b/src/server/request_context.h index 689e5445d..9017a6b28 100644 --- a/src/server/request_context.h +++ b/src/server/request_context.h @@ -99,7 +99,7 @@ class RequestContext { std::string get_query(F filter, bool mustEncode) const { std::string q; const char* sep = ""; - auto encode = [=](const std::string& value) { return mustEncode?urlEncode(value, true):value; }; + auto encode = [=](const std::string& value) { return mustEncode?urlEncode(value):value; }; for ( const auto& a : arguments ) { if (!filter(a.first)) { continue; diff --git a/src/tools/otherTools.cpp b/src/tools/otherTools.cpp index 37345e7d1..243a9a00a 100644 --- a/src/tools/otherTools.cpp +++ b/src/tools/otherTools.cpp @@ -322,9 +322,6 @@ kainjow::mustache::data kiwix::onlyAsNonEmptyMustacheValue(const std::string& s) std::string kiwix::render_template(const std::string& template_str, kainjow::mustache::data data) { kainjow::mustache::mustache tmpl(template_str); - kainjow::mustache::data urlencode{kainjow::mustache::lambda2{ - [](const std::string& str,const kainjow::mustache::renderer& r) { return urlEncode(r(str), true); }}}; - data.set("urlencoded", urlencode); std::stringstream ss; tmpl.render(data, [&ss](const std::string& str) { ss << str; }); return ss.str(); diff --git a/src/tools/stringTools.cpp b/src/tools/stringTools.cpp index dcd5fc4ff..3ca8124aa 100644 --- a/src/tools/stringTools.cpp +++ b/src/tools/stringTools.cpp @@ -161,15 +161,14 @@ std::string kiwix::encodeDiples(const std::string& str) return result; } -/* urlEncode() based on javascript encodeURI() & - encodeURIComponent(). Mostly code from rstudio/httpuv (GPLv3) */ +namespace +{ bool isReservedUrlChar(char c) { switch (c) { case ';': case ',': - case '/': case '?': case ':': case '@': @@ -177,22 +176,22 @@ bool isReservedUrlChar(char c) case '=': case '+': case '$': + case '#': return true; default: return false; } } -bool needsEscape(char c, bool encodeReserved) +bool isHarmlessUriChar(char c) { if (c >= 'a' && c <= 'z') - return false; + return true; if (c >= 'A' && c <= 'Z') - return false; + return true; if (c >= '0' && c <= '9') - return false; - if (isReservedUrlChar(c)) - return encodeReserved; + return true; + switch (c) { case '-': case '_': @@ -203,9 +202,10 @@ bool needsEscape(char c, bool encodeReserved) case '\'': case '(': case ')': - return false; + case '/': + return true; } - return true; + return false; } int hexToInt(char c) { @@ -230,18 +230,18 @@ int hexToInt(char c) { } } -std::string kiwix::urlEncode(const std::string& value, bool encodeReserved) +} // unnamed namespace + +std::string kiwix::urlEncode(const std::string& value) { std::ostringstream os; os << std::hex << std::uppercase; - for (std::string::const_iterator it = value.begin(); - it != value.end(); - it++) { - - if (!needsEscape(*it, encodeReserved)) { - os << *it; + for (const char c : value) { + if (isHarmlessUriChar(c)) { + os << c; } else { - os << '%' << std::setw(2) << static_cast(static_cast(*it)); + const unsigned int charVal = static_cast(c); + os << '%' << std::setw(2) << std::setfill('0') << charVal; } } return os.str(); @@ -267,15 +267,15 @@ std::string kiwix::urlDecode(const std::string& value, bool component) int iHi = hexToInt(hi); int iLo = hexToInt(lo); if (iHi < 0 || iLo < 0) { - // Invalid escape sequence - os << '%' << hi << lo; - continue; + // Invalid escape sequence + os << '%' << hi << lo; + continue; } char c = (char)(iHi << 4 | iLo); if (!component && isReservedUrlChar(c)) { - os << '%' << hi << lo; + os << '%' << hi << lo; } else { - os << c; + os << c; } } else { os << *it; diff --git a/src/tools/stringTools.h b/src/tools/stringTools.h index 3de1f086b..1f55a22bc 100644 --- a/src/tools/stringTools.h +++ b/src/tools/stringTools.h @@ -55,7 +55,9 @@ class ICULanguageInfo }; -std::string urlEncode(const std::string& value, bool encodeReserved = false); +/* urlEncode() is the equivalent of JS encodeURIComponent(), with the only + * difference that the slash (/) symbol is NOT encoded. */ +std::string urlEncode(const std::string& value); std::string urlDecode(const std::string& value, bool component = false); std::string join(const std::vector& list, const std::string& sep); diff --git a/test/library_server.cpp b/test/library_server.cpp index f61d94281..a8f349ef0 100644 --- a/test/library_server.cpp +++ b/test/library_server.cpp @@ -193,7 +193,7 @@ TEST_F(LibraryServerTest, catalog_search_by_phrase) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (q="ray charles")\n" + " Filtered zims (q=%22ray%20charles%22)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -212,7 +212,7 @@ TEST_F(LibraryServerTest, catalog_search_by_words) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (q=ray charles)\n" + " Filtered zims (q=ray%20charles)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 3\n" " 0\n" @@ -233,7 +233,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (q=description:ray description:charles)\n" + " Filtered zims (q=description%3Aray%20description%3Acharles)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -250,7 +250,7 @@ TEST_F(LibraryServerTest, catalog_prefix_search) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (q=title:"ray charles")\n" + " Filtered zims (q=title%3A%22ray%20charles%22)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" @@ -269,7 +269,7 @@ TEST_F(LibraryServerTest, catalog_search_with_word_exclusion) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (q=ray -uncategorized)\n" + " Filtered zims (q=ray%20-uncategorized)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -288,7 +288,7 @@ TEST_F(LibraryServerTest, catalog_search_by_tag) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (tag=_category:jazz)\n" + " Filtered zims (tag=_category%3Ajazz)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" @@ -342,7 +342,7 @@ TEST_F(LibraryServerTest, catalog_search_by_language) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (lang=eng,fra)\n" + " Filtered zims (lang=eng%2Cfra)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -694,7 +694,7 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_search_terms) EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), CATALOG_V2_ENTRIES_PREAMBLE("?q=%22ray%20charles%22") - " Filtered Entries (q="ray charles")\n" + " Filtered Entries (q=%22ray%20charles%22)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -726,8 +726,8 @@ TEST_F(LibraryServerTest, catalog_v2_entries_filtered_by_language) const auto r = zfs1_->GET("/ROOT/catalog/v2/entries?lang=eng,fra"); EXPECT_EQ(r->status, 200); EXPECT_EQ(maskVariableOPDSFeedData(r->body), - CATALOG_V2_ENTRIES_PREAMBLE("?lang=eng,fra") - " Filtered Entries (lang=eng,fra)\n" + CATALOG_V2_ENTRIES_PREAMBLE("?lang=eng%2Cfra") + " Filtered Entries (lang=eng%2Cfra)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 2\n" " 0\n" @@ -865,7 +865,7 @@ TEST_F(LibraryServerTest, no_name_mapper_returned_catalog_use_uuid_in_link) EXPECT_EQ(maskVariableOPDSFeedData(r->body), OPDS_FEED_TAG " 12345678-90ab-cdef-1234-567890abcdef\n" - " Filtered zims (tag=_category:jazz)\n" + " Filtered zims (tag=_category%3Ajazz)\n" " YYYY-MM-DDThh:mm:ssZ\n" " 1\n" " 0\n" diff --git a/test/opds_catalog.cpp b/test/opds_catalog.cpp index 6647d5a32..a1936a19b 100644 --- a/test/opds_catalog.cpp +++ b/test/opds_catalog.cpp @@ -37,34 +37,34 @@ TEST(OpdsCatalog, getSearchUrl) } { Filter f; - f.query("abc def"); - EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def"); + f.query("abc def#xyz"); + EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%20def%23xyz"); } { Filter f; - f.category("ted"); - EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted"); + f.category("ted&bob"); + EXPECT_SEARCH_URL("/catalog/v2/entries?category=ted%26bob"); } { Filter f; - f.lang("eng"); - EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng"); + f.lang("eng,fra"); + EXPECT_SEARCH_URL("/catalog/v2/entries?lang=eng%2Cfra"); } { Filter f; - f.name("second"); - EXPECT_SEARCH_URL("/catalog/v2/entries?name=second"); + f.name("second?"); + EXPECT_SEARCH_URL("/catalog/v2/entries?name=second%3F"); } { Filter f; - f.acceptTags({"paper", "plastic"}); - EXPECT_SEARCH_URL("/catalog/v2/entries?tag=paper;plastic"); + f.acceptTags({"#paper", "#plastic"}); + EXPECT_SEARCH_URL("/catalog/v2/entries?tag=%23paper%3B%23plastic"); } { Filter f; - f.query("abc"); - f.category("ted"); - EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc&category=ted"); + f.query("abc=123"); + f.category("@ted"); + EXPECT_SEARCH_URL("/catalog/v2/entries?q=abc%3D123&category=%40ted"); } { Filter f; @@ -79,7 +79,7 @@ TEST(OpdsCatalog, getSearchUrl) f.lang("html"); f.name("edsonarantesdonascimento"); f.acceptTags({"body", "script"}); - EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body;script"); + EXPECT_SEARCH_URL("/catalog/v2/entries?q=peru&category=scifi&lang=html&name=edsonarantesdonascimento&tag=body%3Bscript"); } #undef EXPECT_SEARCH_URL } diff --git a/test/server.cpp b/test/server.cpp index 78f56ecdc..1086dfe5b 100644 --- a/test/server.cpp +++ b/test/server.cpp @@ -827,7 +827,7 @@ TEST_F(ServerTest, Http400HtmlError) expected_body==R"(

Invalid request

- The requested URL "/ROOT/search?content=non-existing-book&pattern=a"<script foo>" is not a valid request. + The requested URL "/ROOT/search?content=non-existing-book&pattern=a%22%3Cscript%20foo%3E" is not a valid request.

No such book: non-existing-book @@ -910,7 +910,7 @@ TEST_F(ServerTest, HttpXmlError) /* HTTP status code */ 400, /* expected response XML */ R"( Invalid request -The requested URL "/ROOT/search?format=xml&content=non-existing-book&pattern=a"<script foo>" is not a valid request. +The requested URL "/ROOT/search?format=xml&content=non-existing-book&pattern=a%22%3Cscript%20foo%3E" is not a valid request. No such book: non-existing-book )" }, // There is a flaw in our way to handle query string, we cannot differenciate @@ -1156,7 +1156,7 @@ TEST_F(ServerTest, RandomPageRedirectsToAnExistingArticle) auto g = zfs1_->GET("/ROOT/random?content=zimfile"); ASSERT_EQ(302, g->status); ASSERT_TRUE(g->has_header("Location")); - ASSERT_TRUE(kiwix::startsWith(g->get_header_value("Location"), "/ROOT/content/zimfile/A%2F")); + ASSERT_TRUE(kiwix::startsWith(g->get_header_value("Location"), "/ROOT/content/zimfile/A/")); ASSERT_EQ(getCacheControlHeader(*g), "max-age=0, must-revalidate"); ASSERT_FALSE(g->has_header("ETag")); } @@ -1224,7 +1224,7 @@ TEST_F(ServerTest, BookMainPageIsRedirectedToArticleIndex) auto g = zfs1_->GET("/ROOT/content/zimfile"); ASSERT_EQ(302, g->status); ASSERT_TRUE(g->has_header("Location")); - ASSERT_EQ("/ROOT/content/zimfile/A%2Findex", g->get_header_value("Location")); + ASSERT_EQ("/ROOT/content/zimfile/A/index", g->get_header_value("Location")); } } diff --git a/test/server_search.cpp b/test/server_search.cpp index 3932602ca..e8af5f348 100644 --- a/test/server_search.cpp +++ b/test/server_search.cpp @@ -196,7 +196,7 @@ struct SearchResult const std::vector LARGE_SEARCH_RESULTS = { SEARCH_RESULT( - /*link*/ "/ROOT/content/zimfile/A/Genius_+_Soul_=_Jazz", + /*link*/ "/ROOT/content/zimfile/A/Genius_%2B_Soul_%3D_Jazz", /*title*/ "Genius + Soul = Jazz", /*snippet*/ R"SNIPPET(...Grammy Hall of Fame in 2011. It was re-issued in the UK, first in 1989 on the Castle Communications "Essential Records" label, and by Rhino Records in 1997 on a single CD together with Charles' 1970 My Kind of Jazz. In 2010, Concord Records released a deluxe edition comprising digitally remastered versions of Genius + Soul = Jazz, My Kind of Jazz, Jazz Number II, and My Kind of Jazz Part 3. Professional ratings Review scores Source Rating Allmusic link Warr.org link Encyclopedia of Popular Music...)SNIPPET", /*bookTitle*/ "Ray Charles", @@ -236,7 +236,7 @@ const std::vector LARGE_SEARCH_RESULTS = { ), SEARCH_RESULT( - /*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays:_The_Music_of_Ray_Charles", + /*link*/ "/ROOT/content/zimfile/A/Catchin'_Some_Rays%3A_The_Music_of_Ray_Charles", /*title*/ "Catchin' Some Rays: The Music of Ray Charles", /*snippet*/ R"SNIPPET(...jazz singer Roseanna Vitro, released in August 1997 on the Telarc Jazz label. Catchin' Some Rays: The Music of Ray Charles Studio album by Roseanna Vitro Released August 1997 Recorded March 26, 1997 at Sound on Sound, NYC April 4,1997 at Quad Recording Studios, NYC Genre Vocal jazz Length 61:00 Label Telarc Jazz CD-83419 Producer Paul Wickliffe Roseanna Vitro chronology Passion Dance (1996) Catchin' Some Rays: The Music of Ray Charles (1997) The Time of My Life: Roseanna Vitro Sings the Songs of......)SNIPPET", /*bookTitle*/ "Ray Charles", @@ -244,7 +244,7 @@ const std::vector LARGE_SEARCH_RESULTS = { ), SEARCH_RESULT( - /*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say:_John_Scofield_Plays_the_Music_of_Ray_Charles", + /*link*/ "/ROOT/content/zimfile/A/That's_What_I_Say%3A_John_Scofield_Plays_the_Music_of_Ray_Charles", /*title*/ "That's What I Say: John Scofield Plays the Music of Ray Charles", /*snippet*/ R"SNIPPET(That's What I Say: John Scofield Plays the Music of Ray Charles Studio album by John Scofield Released June 7, 2005 (2005-06-07) Recorded December 2004 Studio Avatar Studios, New York City Genre Jazz Length 65:21 Label Verve Producer Steve Jordan John Scofield chronology EnRoute: John Scofield Trio LIVE (2004) That's What I Say: John Scofield Plays the Music of Ray Charles (2005) Out Louder (2006) Professional ratings Review scores Source Rating Allmusic All About Jazz All About Jazz...)SNIPPET", /*bookTitle*/ "Ray Charles", @@ -284,7 +284,7 @@ const std::vector LARGE_SEARCH_RESULTS = { ), SEARCH_RESULT( - /*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again:_Celebrating_the_Genius_of_Ray_Charles", + /*link*/ "/ROOT/content/zimfile/A/Here_We_Go_Again%3A_Celebrating_the_Genius_of_Ray_Charles", /*title*/ "Here We Go Again: Celebrating the Genius of Ray Charles", /*snippet*/ R"SNIPPET(...and jazz trumpeter Wynton Marsalis. It was recorded during concerts at the Rose Theater in New York City, on February 9 and 10, 2009. The album received mixed reviews, in which the instrumentation of Marsalis' orchestra was praised by the critics. Here We Go Again: Celebrating the Genius of Ray Charles Live album by Willie Nelson and Wynton Marsalis Released March 29, 2011 (2011-03-29) Recorded February 9 –10 2009 Venue Rose Theater, New York Genre Jazz, country Length 61:49 Label Blue Note......)SNIPPET", /*bookTitle*/ "Ray Charles", @@ -356,7 +356,7 @@ const std::vector LARGE_SEARCH_RESULTS = { ), SEARCH_RESULT( - /*link*/ "/ROOT/content/zimfile/A/Ray_Sings,_Basie_Swings", + /*link*/ "/ROOT/content/zimfile/A/Ray_Sings%2C_Basie_Swings", /*title*/ "Ray Sings, Basie Swings", /*snippet*/ R"SNIPPET(...from 1973 with newly recorded instrumental tracks by the contemporary Count Basie Orchestra. Professional ratings Review scores Source Rating AllMusic Ray Sings, Basie Swings Compilation album by Ray Charles, Count Basie Orchestra Released October 3, 2006 (2006-10-03) Recorded Mid-1970s, February - May 2006 Studio Los Angeles Genre Soul, jazz, Swing Label Concord/Hear Music Producer Gregg Field Ray Charles chronology Genius & Friends (2005) Ray Sings, Basie Swings (2006) Rare Genius: The Undiscovered Masters (2010)...)SNIPPET", /*bookTitle*/ "Ray Charles", diff --git a/test/stringTools.cpp b/test/stringTools.cpp index 7402f6e0d..27cc712b8 100644 --- a/test/stringTools.cpp +++ b/test/stringTools.cpp @@ -105,4 +105,62 @@ TEST(stringTools, extractFromString) ASSERT_THROW(extractFromString("3.14.5"), std::invalid_argument); } +namespace URLEncoding +{ + +const char letters[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const char digits[] = "0123456789"; +const char nonEncodableSymbols[] = ".-_~()*!/"; +const char uriDelimSymbols[] = ":@?=+&#$;,"; + +const char otherSymbols[] = R"(`%^[]{}\|"<>)"; + +const char whitespace[] = " \n\t\r"; + +const char someNonASCIIChars[] = "Σ♂♀ツ"; + +} + +TEST(stringTools, urlEncode) +{ + using namespace URLEncoding; + + EXPECT_EQ(urlEncode(letters), letters); + + EXPECT_EQ(urlEncode(digits), digits); + + EXPECT_EQ(urlEncode(nonEncodableSymbols), nonEncodableSymbols); + + EXPECT_EQ(urlEncode(uriDelimSymbols), "%3A%40%3F%3D%2B%26%23%24%3B%2C"); + + EXPECT_EQ(urlEncode(otherSymbols), "%60%25%5E%5B%5D%7B%7D%5C%7C%22%3C%3E"); + + EXPECT_EQ(urlEncode(whitespace), "%20%0A%09%0D"); + + EXPECT_EQ(urlEncode(someNonASCIIChars), "%CE%A3%E2%99%82%E2%99%80%E3%83%84"); +} + +TEST(stringTools, urlDecode) +{ + using namespace URLEncoding; + + const std::string allTestChars = std::string(letters) + + digits + + nonEncodableSymbols + + uriDelimSymbols + + otherSymbols + + whitespace + + someNonASCIIChars; + + for ( const char c : allTestChars ) { + const std::string str(1, c); + EXPECT_EQ(urlDecode(urlEncode(str), true), str); + } + + EXPECT_EQ(urlDecode(urlEncode(allTestChars), true), allTestChars); + + const std::string encodedUriDelimSymbols = urlEncode(uriDelimSymbols); + EXPECT_EQ(urlDecode(encodedUriDelimSymbols, false), encodedUriDelimSymbols); +} + };