Escape special characters for like sql searches

This commit is contained in:
emeric
2021-05-02 15:10:32 +02:00
parent a6398738bb
commit 444d4af3c6
27 changed files with 261 additions and 68 deletions
+1 -1
View File
@@ -251,7 +251,7 @@ guessMediaFileFormat(const std::filesystem::path& file)
if (formats.size() > 1)
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'";
std::vector<std::string> mimeTypes;
std::vector<std::string_view> mimeTypes;
if (format->mime_type)
mimeTypes = StringUtils::splitString(format->mime_type, ",");
+1
View File
@@ -13,6 +13,7 @@ add_library(lmsdatabase SHARED
impl/Track.cpp
impl/TrackBookmark.cpp
impl/User.cpp
impl/Utils.cpp
)
target_include_directories(lmsdatabase INTERFACE
+9 -9
View File
@@ -27,7 +27,7 @@
#include "database/User.hpp"
#include "utils/Logger.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
namespace Database
{
@@ -83,7 +83,7 @@ Wt::Dbo::Query<T>
createQuery(Session& session,
const std::string& queryStr,
const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords,
const std::vector<std::string_view>& keywords,
std::optional<TrackArtistLinkType> linkType)
{
session.checkSharedLocked();
@@ -100,16 +100,16 @@ createQuery(Session& session,
std::vector<std::string> clauses;
std::vector<std::string> sortClauses;
for (const std::string& keyword : keywords)
for (std::string_view keyword : keywords)
{
clauses.push_back("a.name LIKE ?");
query.bind("%%" + keyword + "%%");
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%%" + escapeLikeKeyword(keyword) + "%%");
}
for (const std::string& keyword : keywords)
for (std::string_view keyword : keywords)
{
sortClauses.push_back("a.sort_name LIKE ?");
query.bind("%%" + keyword + "%%");
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%%" + escapeLikeKeyword(keyword) + "%%");
}
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
@@ -267,7 +267,7 @@ Artist::getByClusters(Session& session, const std::set<IdType>& clusters, SortMe
std::vector<Artist::pointer>
Artist::getByFilter(Session& session,
const std::set<IdType>& clusters,
const std::vector<std::string>& keywords,
const std::vector<std::string_view>& keywords,
std::optional<TrackArtistLinkType> linkType,
SortMethod sortMethod,
std::optional<Range> range,
+5 -4
View File
@@ -28,6 +28,7 @@
#include "database/User.hpp"
#include "utils/Logger.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
namespace Database
{
@@ -38,14 +39,14 @@ Wt::Dbo::Query<T>
createQuery(Session& session,
const std::string& queryStr,
const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords)
const std::vector<std::string_view>& keywords)
{
auto query {session.getDboSession().query<T>(queryStr)};
query.join("track t ON t.release_id = r.id");
for (const std::string& keyword : keywords)
query.where("r.name LIKE ?").bind("%%" + keyword + "%%");
for (std::string_view keyword : keywords)
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%%" + escapeLikeKeyword(keyword) + "%%");
if (!clusterIds.empty())
{
@@ -300,7 +301,7 @@ Release::getByClusters(Session& session, const std::set<IdType>& clusters)
std::vector<Release::pointer>
Release::getByFilter(Session& session,
const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords,
const std::vector<std::string_view>& keywords,
std::optional<Range> range,
bool& moreResults)
{
+5 -4
View File
@@ -31,6 +31,7 @@
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
namespace Database {
@@ -40,14 +41,14 @@ Wt::Dbo::Query<T>
createQuery(Session& session,
const std::string& queryStr,
const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords)
const std::vector<std::string_view>& keywords)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<T>(queryStr)};
for (const std::string& keyword : keywords)
query.where("t.name LIKE ?").bind("%%" + keyword + "%%");
for (std::string_view keyword : keywords)
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%%" + escapeLikeKeyword(keyword) + "%%");
if (!clusterIds.empty())
{
@@ -333,7 +334,7 @@ Track::hasTrackFeatures() const
std::vector<Track::pointer>
Track::getByFilter(Session& session,
const std::set<IdType>& clusterIds,
const std::vector<std::string>& keywords,
const std::vector<std::string_view>& keywords,
std::optional<Range> range,
bool& moreResults)
{
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Utils.hpp"
#include "utils/String.hpp"
namespace Database
{
std::string
escapeLikeKeyword(std::string_view keyword)
{
return StringUtils::escapeString(keyword, "*_", escapeChar);
}
} // namespace Database
+33
View File
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <string_view>
#include <vector>
namespace Database
{
#define ESCAPE_CHAR_STR "\\"
static constexpr char escapeChar {'\\'};
std::string escapeLikeKeyword(std::string_view keywords);
} // namespace Database
@@ -21,6 +21,7 @@
#include <optional>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>
@@ -69,7 +70,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
);
static std::vector<pointer> getByFilter(Session& session,
const std::set<IdType>& clusters, // if non empty, at least one artist that belongs to these clusters
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
SortMethod sortMethod,
std::optional<Range> range,
@@ -66,7 +66,7 @@ class Release : public Wt::Dbo::Dbo<Release>
static std::vector<pointer> getByClusters(Session& session, const std::set<IdType>& clusters);
static std::vector<pointer> getByFilter(Session& session,
const std::set<IdType>& clusters, // if non empty, at least one release that belongs to these clusters
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords
std::optional<Range> range,
bool& moreExpected);
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
+1 -1
View File
@@ -71,7 +71,7 @@ class Track : public Wt::Dbo::Dbo<Track>
const std::set<IdType>& clusters); // tracks that belong to these clusters
static std::vector<pointer> getByFilter(Session& session,
const std::set<IdType>& clusters, // if non empty, tracks that belong to these clusters
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords
std::optional<Range> range,
bool& moreExpected);
static std::vector<pointer> getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName);
+13 -9
View File
@@ -49,10 +49,10 @@ findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initiali
if (!str)
return std::nullopt;
std::vector<std::string> strUuids = StringUtils::splitString(*str, "/");
const std::vector<std::string_view> strUuids {StringUtils::splitString(*str, "/")};
std::vector<UUID> res;
for (const std::string& strUuid : strUuids)
for (std::string_view strUuid : strUuids)
{
std::optional<UUID> uuid {UUID::fromString(strUuid)};
if (!uuid)
@@ -101,7 +101,7 @@ getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
{
std::vector<Artist> artists;
std::vector<std::string> artistNames;
std::vector<std::string_view> artistNames;
if (metadataMap.find("ARTISTS") != metadataMap.end())
{
artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
@@ -161,8 +161,7 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
else if (tag == "TRACK")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {StringUtils::splitString(value, "/") };
const std::vector<std::string_view> strings {StringUtils::splitString(value, "/") };
if (strings.size() > 0)
{
track.trackNumber = StringUtils::readAs<std::size_t>(strings[0]);
@@ -174,8 +173,7 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
else if (tag == "DISC")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {StringUtils::splitString(value, "/")};
const std::vector<std::string_view> strings {StringUtils::splitString(value, "/")};
if (strings.size() > 0)
{
track.discNumber = StringUtils::readAs<std::size_t>(strings[0]);
@@ -217,10 +215,16 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
}
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
{
std::vector<std::string> clusterNames {StringUtils::splitString(value, "/,;")};
const std::vector<std::string_view> clusterNames {StringUtils::splitString(value, "/,;")};
if (!clusterNames.empty())
track.clusters[tag] = std::set<std::string>{clusterNames.begin(), clusterNames.end()};
{
std::set<std::string> values;
std::transform(std::cbegin(clusterNames), std::cend(clusterNames),
std::inserter(values, std::begin(values)),
[](std::string_view clusterName) { return std::string {clusterName}; });
track.clusters[tag] = std::move(values);
}
}
}
+4 -5
View File
@@ -78,12 +78,12 @@ getPropertyValuesAs(const TagLib::PropertyMap& properties, const std::string& ke
static
std::vector<std::string>
splitAndTrimString(const std::string& str, const std::string& delimiters)
splitAndTrimString(const std::string& str, std::string_view delimiters)
{
std::vector<std::string> res;
std::vector<std::string> strings {StringUtils::splitString(str, delimiters)};
for (const std::string& s : strings)
std::vector<std::string_view> strings {StringUtils::splitString(str, delimiters)};
for (std::string_view s : strings)
res.emplace_back(StringUtils::stringTrim(s));
return res;
@@ -203,8 +203,7 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
else if (tag == "DISCNUMBER")
{
// Expecting 'Number/Total'
std::vector<std::string> strings {StringUtils::splitString(value, "/")};
std::vector<std::string_view> strings {StringUtils::splitString(value, "/")};
if (!strings.empty())
{
track.discNumber = StringUtils::readAs<std::size_t>(strings[0]);
@@ -30,7 +30,7 @@ namespace StringUtils
{
template<>
std::optional<std::chrono::seconds>
readAs(const std::string& str)
readAs(std::string_view str)
{
std::optional<std::chrono::seconds> res;
+2 -2
View File
@@ -23,14 +23,14 @@ namespace StringUtils
{
template<>
std::optional<API::Subsonic::Id>
readAs(const std::string& str)
readAs(std::string_view str)
{
return API::Subsonic::IdFromString(str);
}
template<>
std::optional<bool>
readAs(const std::string& str)
readAs(std::string_view str)
{
if (str == "true")
return true;
+2 -2
View File
@@ -90,10 +90,10 @@ namespace StringUtils
{
template<>
std::optional<API::Subsonic::Id>
readAs(const std::string& str);
readAs(std::string_view str);
template<>
std::optional<bool>
readAs(const std::string& str);
readAs(std::string_view str);
}
+2 -2
View File
@@ -28,12 +28,12 @@ namespace API::Subsonic
{
std::optional<Id>
IdFromString(const std::string& id)
IdFromString(const std::string_view id)
{
if (id == "root")
return Id {Id::Type::Root};
std::vector<std::string> values {StringUtils::splitString(id, "-")};
std::vector<std::string_view> values {StringUtils::splitString(id, "-")};
if (values.size() != 2)
return std::nullopt;
+1 -1
View File
@@ -41,7 +41,7 @@ struct Id
Database::IdType value {};
};
std::optional<Id> IdFromString(const std::string& id);
std::optional<Id> IdFromString(std::string_view id);
std::string IdToString(const Id& id);
} // namespace API::Subsonic
+2 -2
View File
@@ -71,7 +71,7 @@ namespace StringUtils
{
template<>
std::optional<API::Subsonic::ClientVersion>
readAs(const std::string& str)
readAs(std::string_view str)
{
// Expects "X.Y.Z"
const auto numbers {StringUtils::splitString(str, ".")};
@@ -1454,7 +1454,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
// Mandatory params
std::string query {getMandatoryParameterAs<std::string>(context.parameters, "query")};
std::vector<std::string> keywords {StringUtils::splitString(query, " ")};
std::vector<std::string_view> keywords {StringUtils::splitString(query, " ")};
// Optional params
std::size_t artistCount {getParameterAs<std::size_t>(context.parameters, "artistCount").value_or(20)};
+51 -10
View File
@@ -19,6 +19,7 @@
#include "utils/String.hpp"
#include <algorithm>
#include <iomanip>
#include <unordered_map>
@@ -57,13 +58,13 @@ readList(const std::string& str, const std::string& separators, std::list<std::s
template<>
std::optional<std::string>
readAs(const std::string& str)
readAs(std::string_view str)
{
return str;
return std::string {str};
}
std::vector<std::string>
splitString(const std::string& string, const std::string& separators)
splitStringCopy(std::string_view string, std::string_view separators)
{
std::string str {stringTrim(string, separators)};
@@ -73,6 +74,29 @@ splitString(const std::string& string, const std::string& separators)
return res;
}
std::vector<std::string_view>
splitString(std::string_view str, std::string_view separators)
{
std::vector<std::string_view> res;
std::string_view::size_type strBegin {};
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
{
auto strEnd {str.find_first_of(separators, strBegin + 1)};
if (strEnd == std::string_view::npos)
{
res.push_back(str.substr(strBegin, str.size() - strBegin));
break;
}
res.push_back(str.substr(strBegin, strEnd - strBegin));
strBegin = strEnd + 1;
}
return res;
}
std::string
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
{
@@ -80,22 +104,22 @@ joinStrings(const std::vector<std::string>& strings, const std::string& delimite
}
std::string
stringTrim(const std::string& str, const std::string& whitespace)
stringTrim(std::string_view str, std::string_view whitespaces)
{
const auto strBegin = str.find_first_not_of(whitespace);
if (strBegin == std::string::npos)
const auto strBegin = str.find_first_not_of(whitespaces);
if (strBegin == std::string_view::npos)
return ""; // no content
const auto strEnd = str.find_last_not_of(whitespace);
const auto strEnd = str.find_last_not_of(whitespaces);
const auto strRange = strEnd - strBegin + 1;
return str.substr(strBegin, strRange);
return std::string {str.substr(strBegin, strRange)};
}
std::string
stringTrimEnd(const std::string& str, const std::string& whitespace)
stringTrimEnd(std::string_view str, std::string_view whitespaces)
{
return str.substr(0, str.find_last_not_of(whitespace)+1);
return std::string {str.substr(0, str.find_last_not_of(whitespaces) + 1)};
}
std::string
@@ -185,6 +209,23 @@ jsEscape(const std::string& str)
return escaped;
}
std::string
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
{
std::string res;
res.reserve(str.size());
for (const char c : str)
{
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
res += escapeChar;
res += c;
}
return res;
}
bool
stringEndsWith(const std::string& str, const std::string& ending)
{
+1 -1
View File
@@ -27,7 +27,7 @@ namespace StringUtils
{
template <>
std::optional<UUID>
readAs(const std::string& str)
readAs(std::string_view str)
{
return UUID::fromString(str);
}
+13 -7
View File
@@ -19,6 +19,7 @@
#pragma once
#include <initializer_list>
#include <optional>
#include <string>
#include <string_view>
@@ -31,16 +32,19 @@
namespace StringUtils {
std::vector<std::string>
splitString(const std::string& string, const std::string& separators);
splitStringCopy(std::string_view string, std::string_view separators);
std::vector<std::string_view>
splitString(std::string_view string, std::string_view separators);
std::string
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
std::string
stringTrim(const std::string& str, const std::string& whitespaces = " \t");
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
std::string
stringTrimEnd(const std::string& str, const std::string& whitespaces = " \t");
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
std::string
stringToLower(std::string_view str);
@@ -55,11 +59,11 @@ std::string
bufferToString(const std::vector<unsigned char>& data);
template<typename T>
std::optional<T> readAs(const std::string& str)
std::optional<T> readAs(std::string_view str)
{
T res;
std::istringstream iss ( str );
std::istringstream iss {std::string {str}};
iss >> res;
if (iss.fail())
return std::nullopt;
@@ -69,15 +73,17 @@ std::optional<T> readAs(const std::string& str)
template<>
std::optional<std::string>
readAs(const std::string& str);
readAs(std::string_view str);
[[nodiscard]]
std::string
replaceInString(const std::string& str, const std::string& from, const std::string& to);
std::string
jsEscape(const std::string& str);
std::string
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
bool
stringEndsWith(const std::string& str, const std::string& ending);
+1 -1
View File
@@ -42,6 +42,6 @@ namespace StringUtils
{
template <>
std::optional<UUID>
readAs(const std::string& str);
readAs(std::string_view str);
}
+1 -1
View File
@@ -139,7 +139,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
if (recommendationEngineTypeRow)
scanSettings.modify()->setRecommendationEngineType(_recommendationEngineTypeModel->getValue(*recommendationEngineTypeRow));
auto clusterTypes {StringUtils::splitString(valueText(TagsField).toUTF8(), " ")};
auto clusterTypes {StringUtils::splitStringCopy(valueText(TagsField).toUTF8(), " ")};
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string>(clusterTypes.begin(), clusterTypes.end()));
}
+3 -1
View File
@@ -53,7 +53,8 @@ namespace UserInterface
void
SearchView::refreshView(const Wt::WString& searchText)
{
_keywords = StringUtils::splitString(searchText.toUTF8(), " ");
_searchValue = searchText.toUTF8();
_keywords = StringUtils::splitString(_searchValue, " ");
refreshView();
}
@@ -73,6 +74,7 @@ namespace UserInterface
SearchView::searchArtists()
{
bool more;
const auto artists {Database::Artist::getByFilter(LmsApp->getDbSession(),
_filters->getClusterIds(),
_keywords,
+3 -1
View File
@@ -20,6 +20,7 @@
#pragma once
#include <string>
#include <string_view>
#include <vector>
#include <Wt/WTemplate.h>
@@ -46,7 +47,8 @@ class SearchView : public Wt::WTemplate
void searchTracks();
Filters* _filters {};
std::vector<std::string> _keywords;
std::string _searchValue;
std::vector<std::string_view> _keywords;
};
} // namespace UserInterface
@@ -39,7 +39,7 @@ namespace StringUtils
{
template <>
std::optional<Database::AudioFormat>
readAs(const std::string& str)
readAs(std::string_view str)
{
auto encodedFormat {readAs<int>(str)};
+69
View File
@@ -26,6 +26,74 @@
#include <stdlib.h>
#include "utils/RecursiveSharedMutex.hpp"
#include "utils/String.hpp"
void
testStrings()
{
{
const std::string test{"a"};
const std::vector<std::string_view> strings {StringUtils::splitString(test, "")};
assert(strings.size() == 1);
assert(strings.front() == "a");
}
{
const std::string test{"a b"};
const std::vector<std::string_view> strings {StringUtils::splitString(test, "|")};
assert(strings.size() == 1);
assert(strings.front() == "a b");
}
{
const std::string test{" a"};
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
assert(strings.size() == 1);
assert(strings.front() == "a");
}
{
const std::string test{"a "};
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
assert(strings.size() == 1);
assert(strings.front() == "a");
}
{
const std::string test{"a b"};
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ")};
assert(strings.size() == 2);
assert(strings.front() == "a");
assert(strings.back() == "b");
}
{
const std::string test{"a b,c|defgh "};
const std::vector<std::string_view> strings {StringUtils::splitString(test, " ,|")};
assert(strings.size() == 4);
assert(strings[0] == "a");
assert(strings[1] == "b");
assert(strings[2] == "c");
assert(strings[3] == "defgh");
}
{
assert(StringUtils::escapeString("", "*", ' ') == "");
assert(StringUtils::escapeString("", "", ' ') == "");
assert(StringUtils::escapeString("a", "", ' ') == "a");
assert(StringUtils::escapeString("*", "*", '_') == "_*");
assert(StringUtils::escapeString("*a*", "*", '_') == "_*a_*");
assert(StringUtils::escapeString("*a|", "*|", '_') == "_*a_|");
assert(StringUtils::escapeString("**||", "*|", '_') == "_*_*_|_|");
}
}
void
testSharedMutex()
@@ -109,6 +177,7 @@ int main()
{
try
{
testStrings();
testSharedMutex();
}
catch (std::exception& e)