1 #ifndef LIBFILEZILLA_STRING_HEADER
2 #define LIBFILEZILLA_STRING_HEADER
3 
4 #include "libfilezilla.hpp"
5 
6 #include <algorithm>
7 #include <string>
8 #include <string_view>
9 #include <vector>
10 
11 /** \file
12  * \brief String types and assorted functions.
13  *
14  * Defines the \ref fz::native_string type and offers various functions to convert between
15  * different string types.
16  */
17 
18 namespace fz {
19 
20 /** \typedef native_string
21  *
22  * \brief A string in the system's native character type and encoding.\n Note: This typedef changes depending on platform!
23  *
24  * On Windows, the system's native encoding is UTF-16, so native_string is typedef'ed to std::wstring.
25  *
26  * On all other platform, native_string is a typedef for std::string.
27  *
28  * Always using native_string has the benefit that no conversion needs to be performed which is especially useful
29  * if dealing with filenames.
30  */
31 
32 #ifdef FZ_WINDOWS
33 typedef std::wstring native_string;
34 typedef std::wstring_view native_string_view;
35 #endif
36 #if defined(FZ_UNIX) || defined(FZ_MAC)
37 typedef std::string native_string;
38 typedef std::string_view native_string_view;
39 #endif
40 
41 /** \brief Converts std::string to native_string.
42  *
43  * \return the converted string on success. On failure an empty string is returned.
44  */
45 native_string FZ_PUBLIC_SYMBOL to_native(std::string_view const& in);
46 
47 /** \brief Convert std::wstring to native_string.
48  *
49  * \return the converted string on success. On failure an empty string is returned.
50  */
51 native_string FZ_PUBLIC_SYMBOL to_native(std::wstring_view const& in);
52 
53 /// Avoid converting native_string to native_string_view and back to native_string
54 template<typename T, typename std::enable_if_t<std::is_same_v<native_string, typename std::decay_t<T>>, int> = 0>
to_native(T const & in)55 inline native_string to_native(T const& in) {
56 	return in;
57 }
58 
59 /** \brief Locale-sensitive stricmp
60  *
61  * Like std::string::compare but case-insensitive, respecting locale.
62  *
63  * \note does not handle embedded null
64  */
65 int FZ_PUBLIC_SYMBOL stricmp(std::string_view const& a, std::string_view const& b);
66 int FZ_PUBLIC_SYMBOL stricmp(std::wstring_view const& a, std::wstring_view const& b);
67 
68 /** \brief Converts ASCII uppercase characters to lowercase as if C-locale is used.
69 
70  Under some locales there is a different case-relationship
71  between the letters a-z and A-Z as one expects from ASCII under the C locale.
72  In Turkish for example there are different variations of the letter i,
73  namely dotted and dotless. What we see as 'i' is the lowercase dotted i and
74  'I' is the  uppercase dotless i. Since std::tolower is locale-aware, I would
75  become the dotless lowercase i.
76 
77  This is not always what we want. FTP commands for example are case-insensitive
78  ASCII strings, LIST and list are the same.
79 
80  tolower_ascii instead converts all types of 'i's to the ASCII i as well.
81 
82  \return  A-Z becomes a-z.\n In addition dotless lowercase i and dotted uppercase i also become the standard i.
83 
84  */
85 template<typename Char>
tolower_ascii(Char c)86 Char tolower_ascii(Char c) {
87 	if (c >= 'A' && c <= 'Z') {
88 		return c + ('a' - 'A');
89 	}
90 	return c;
91 }
92 
93 template<>
94 std::wstring::value_type FZ_PUBLIC_SYMBOL tolower_ascii(std::wstring::value_type c);
95 
96 /// \brief Converts ASCII lowercase characters to uppercase as if C-locale is used.
97 template<typename Char>
toupper_ascii(Char c)98 Char toupper_ascii(Char c) {
99 	if (c >= 'a' && c <= 'z') {
100 		return c + ('A' - 'a');
101 	}
102 	return c;
103 }
104 
105 template<>
106 std::wstring::value_type FZ_PUBLIC_SYMBOL toupper_ascii(std::wstring::value_type c);
107 
108 /** \brief tr_tolower_ascii does for strings what tolower_ascii does for individual characters
109  */
110  // Note: For UTF-8 strings it works on individual octets!
111 std::string FZ_PUBLIC_SYMBOL str_tolower_ascii(std::string_view const& s);
112 std::wstring FZ_PUBLIC_SYMBOL str_tolower_ascii(std::wstring_view const& s);
113 
114 std::string FZ_PUBLIC_SYMBOL str_toupper_ascii(std::string_view const& s);
115 std::wstring FZ_PUBLIC_SYMBOL str_toupper_ascii(std::wstring_view const& s);
116 
117 /** \brief Comparator to be used for std::map for case-insensitive keys
118  *
119  * Comparison is done locale-agnostic.
120  * Useful for key-value pairs in protocols, e.g. HTTP headers.
121  */
122 struct FZ_PUBLIC_SYMBOL less_insensitive_ascii final
123 {
124 	template<typename T>
operator ()fz::less_insensitive_ascii125 	bool operator()(T const& lhs, T const& rhs) const {
126 		return std::lexicographical_compare(lhs.cbegin(), lhs.cend(), rhs.cbegin(), rhs.cend(),
127 		    [](typename T::value_type const& a, typename T::value_type const& b) {
128 			    return tolower_ascii(a) < tolower_ascii(b);
129 		    }
130 		);
131 	}
132 };
133 
134 /** \brief Locale-insensitive stricmp
135  *
136  * Equivalent to str_tolower_ascii(a).compare(str_tolower_ascii(b));
137  */
equal_insensitive_ascii(std::string_view a,std::string_view b)138 inline bool equal_insensitive_ascii(std::string_view a, std::string_view b)
139 {
140 	return std::equal(a.cbegin(), a.cend(), b.cbegin(), b.cend(),
141 	    [](auto const& a, auto const& b) {
142 		    return tolower_ascii(a) == tolower_ascii(b);
143 	    }
144 	);
145 }
equal_insensitive_ascii(std::wstring_view a,std::wstring_view b)146 inline bool equal_insensitive_ascii(std::wstring_view a, std::wstring_view b)
147 {
148 	return std::equal(a.cbegin(), a.cend(), b.cbegin(), b.cend(),
149 	    [](auto const& a, auto const& b) {
150 		    return tolower_ascii(a) == tolower_ascii(b);
151 	    }
152 	);
153 }
154 
155 /** \brief Converts from std::string in system encoding into std::wstring
156  *
157  * \return the converted string on success. On failure an empty string is returned.
158  */
159 std::wstring FZ_PUBLIC_SYMBOL to_wstring(std::string_view const& in);
to_wstring(std::wstring_view const & in)160 inline std::wstring FZ_PUBLIC_SYMBOL to_wstring(std::wstring_view const& in) { return std::wstring(in); }
161 
162 /** \brief Returns identity, that way to_wstring can be called with native_string.
163  *
164  * This template prevents converting std::wstring to std::wstring_view and back to std::wstring
165  */
166 template<typename T, typename std::enable_if_t<std::is_same_v<std::wstring, typename std::decay_t<T>>, int> = 0>
to_wstring(T const & in)167 inline std::wstring to_wstring(T const& in) {
168 	return in;
169 }
170 
171 /// Converts from arithmetic type to std::wstring
172 template<typename Arg>
to_wstring(Arg && arg)173 inline typename std::enable_if<std::is_arithmetic_v<std::decay_t<Arg>>, std::wstring>::type to_wstring(Arg && arg)
174 {
175 	return std::to_wstring(std::forward<Arg>(arg));
176 }
177 
178 
179 /** \brief Converts from std::string in UTF-8 into std::wstring
180  *
181  * \return the converted string on success. On failure an empty string is returned.
182  */
183 std::wstring FZ_PUBLIC_SYMBOL to_wstring_from_utf8(std::string_view const& in);
184 std::wstring FZ_PUBLIC_SYMBOL to_wstring_from_utf8(char const* s, size_t len);
185 
186 class buffer;
187 std::wstring FZ_PUBLIC_SYMBOL to_wstring_from_utf8(fz::buffer const& in);
188 
189 /** \brief Converts from std::wstring into std::string in system encoding
190  *
191  * \return the converted string on success. On failure an empty string is returned.
192  */
193 std::string FZ_PUBLIC_SYMBOL to_string(std::wstring_view const& in);
to_string(std::string_view const & in)194 inline std::string FZ_PUBLIC_SYMBOL to_string(std::string_view const& in) { return std::string(in); }
195 
196 /** \brief Returns identity, that way to_wstring can be called with native_string.
197  *
198  * This template prevents converting std::wstring to std::wstring_view and back to std::wstring
199  */
200 template<typename T, typename std::enable_if_t<std::is_same_v<std::string, typename std::decay_t<T>>, int> = 0>
to_string(T const & in)201 inline std::string to_string(T const& in) {
202 	return in;
203 }
204 
205 
206 /// Converts from arithmetic type to std::string
207 template<typename Arg>
to_string(Arg && arg)208 inline typename std::enable_if<std::is_arithmetic_v<std::decay_t<Arg>>, std::string>::type to_string(Arg && arg)
209 {
210 	return std::to_string(std::forward<Arg>(arg));
211 }
212 
213 
214 /// Returns length of 0-terminated character sequence. Works with both narrow and wide-characters.
215 template<typename Char>
strlen(Char const * str)216 size_t strlen(Char const* str) {
217 	return std::char_traits<Char>::length(str);
218 }
219 
220 
221 /** \brief Converts from std::string in native encoding into std::string in UTF-8
222  *
223  * \return the converted string on success. On failure an empty string is returned.
224  *
225  * \note Does not handle embedded nulls
226  */
227 std::string FZ_PUBLIC_SYMBOL to_utf8(std::string_view const& in);
228 
229 /** \brief Converts from std::wstring in native encoding into std::string in UTF-8
230  *
231  * \return the converted string on success. On failure an empty string is returned.
232  *
233  * \note Does not handle embedded nulls
234  */
235 std::string FZ_PUBLIC_SYMBOL to_utf8(std::wstring_view const& in);
236 
237 /// Calls either fz::to_string or fz::to_wstring depending on the passed template argument
238 template<typename String, typename Arg>
toString(Arg && arg)239 inline auto toString(Arg&& arg) -> typename std::enable_if<std::is_same_v<String, std::string>, decltype(to_string(std::forward<Arg>(arg)))>::type
240 {
241 	return to_string(std::forward<Arg>(arg));
242 }
243 
244 template<typename String, typename Arg>
toString(Arg && arg)245 inline auto toString(Arg&& arg) -> typename std::enable_if<std::is_same_v<String, std::wstring>, decltype(to_wstring(std::forward<Arg>(arg)))>::type
246 {
247 	return to_wstring(std::forward<Arg>(arg));
248 }
249 
250 #if !defined(fzT) || defined(DOXYGEN)
251 #ifdef FZ_WINDOWS
252 /** \brief Macro for a string literal in system-native character type.\n Note: Macro definition changes depending on platform!
253  *
254  * Example: \c fzT("this string is wide on Windows and narrow elsewhere")
255  */
256 #define fzT(x) L ## x
257 #else
258 /** \brief Macro for a string literal in system-native character type.\n Note: Macro definition changes depending on platform!
259  *
260  * Example: \c fzT("this string is wide on Windows and narrow elsewhere")
261  */
262 #define fzT(x) x
263 #endif
264 #endif
265 
266  /// Returns the function argument of the type matching the template argument. \sa fzS
267 template<typename Char>
268 Char const* choose_string(char const* c, wchar_t const* w);
269 
choose_string(char const * c,wchar_t const *)270 template<> inline char const* choose_string(char const* c, wchar_t const*) { return c; }
choose_string(char const *,wchar_t const * w)271 template<> inline wchar_t const* choose_string(char const*, wchar_t const* w) { return w; }
272 
273 #if !defined(fzS) || defined(DOXYGEN)
274 /** \brief Macro to get const pointer to a string of the corresponding type
275  *
276  * Useful when using string literals in templates where the type of string
277  * is a template argument:
278  * \code
279  *   template<typename String>
280  *   String append_foo(String const& s) {
281  *       s += fzS(String::value_type, "foo");
282  *   }
283  * \endcode
284  */
285 #define fzS(Char, s) fz::choose_string<Char>(s, L ## s)
286 #endif
287 
288  /** \brief Returns \c in with all occurrences of \c find in the input string replaced with \c replacement
289   *
290   * \arg find If empty, no replacement takes place.
291   */
292 std::string FZ_PUBLIC_SYMBOL replaced_substrings(std::string_view const& in, std::string_view const& find, std::string_view const& replacement);
293 std::wstring FZ_PUBLIC_SYMBOL replaced_substrings(std::wstring_view const& in, std::wstring_view const& find, std::wstring_view const& replacement);
294 
295 /// Returns \c in with all occurrences of \c find in the input string replaced with \c replacement
296 std::string FZ_PUBLIC_SYMBOL replaced_substrings(std::string_view const& in, char find, char replacement);
297 std::wstring FZ_PUBLIC_SYMBOL replaced_substrings(std::wstring_view const& in, wchar_t find, wchar_t replacement);
298 
299 /** \brief Modifies \c in, replacing all occurrences of \c find with \c replacement
300  *
301  * \arg find If empty, no replacement takes place.
302  */
303 bool FZ_PUBLIC_SYMBOL replace_substrings(std::string& in, std::string_view const& find, std::string_view const& replacement);
304 bool FZ_PUBLIC_SYMBOL replace_substrings(std::wstring& in, std::wstring_view const& find, std::wstring_view const& replacement);
305 
306 /// Modifies \c in, replacing all occurrences of \c find with \c replacement
307 bool FZ_PUBLIC_SYMBOL replace_substrings(std::string& in, char find, char replacement);
308 bool FZ_PUBLIC_SYMBOL replace_substrings(std::wstring& in, wchar_t find, wchar_t replacement);
309 
310 /**
311  * \brief Tokenizes string.
312  *
313  * \param delims the delimiters to look for
314  * \param ignore_empty If true, empty tokens are omitted in the output
315  */
316 std::vector<std::string> FZ_PUBLIC_SYMBOL strtok(std::string_view const& tokens, std::string_view const& delims, bool const ignore_empty = true);
317 std::vector<std::wstring> FZ_PUBLIC_SYMBOL strtok(std::wstring_view const& tokens, std::wstring_view const& delims, bool const ignore_empty = true);
strtok(std::string_view const & tokens,char const delim,bool const ignore_empty=true)318 inline auto FZ_PUBLIC_SYMBOL strtok(std::string_view const& tokens, char const delim, bool const ignore_empty = true) {
319 	return strtok(tokens, std::string_view(&delim, 1), ignore_empty);
320 }
strtok(std::wstring_view const & tokens,wchar_t const delim,bool const ignore_empty=true)321 inline auto FZ_PUBLIC_SYMBOL strtok(std::wstring_view const& tokens, wchar_t const delim, bool const ignore_empty = true) {
322 	return strtok(tokens, std::wstring_view(&delim, 1), ignore_empty);
323 }
324 
325 /**
326  * \brief Tokenizes string.
327  *
328  * \warning This function returns string_views, mind the lifetime of the string passed in tokens.
329  *
330  * \param delims the delimiters to look for
331  * \param ignore_empty If true, empty tokens are omitted in the output
332  */
333 std::vector<std::string_view> FZ_PUBLIC_SYMBOL strtok_view(std::string_view const& tokens, std::string_view const& delims, bool const ignore_empty = true);
334 std::vector<std::wstring_view> FZ_PUBLIC_SYMBOL strtok_view(std::wstring_view const& tokens, std::wstring_view const& delims, bool const ignore_empty = true);
strtok_view(std::string_view const & tokens,char const delim,bool const ignore_empty=true)335 inline auto FZ_PUBLIC_SYMBOL strtok_view(std::string_view const& tokens, char const delim, bool const ignore_empty = true) {
336 	return strtok_view(tokens, std::string_view(&delim, 1), ignore_empty);
337 }
strtok_view(std::wstring_view const & tokens,wchar_t const delim,bool const ignore_empty=true)338 inline auto FZ_PUBLIC_SYMBOL strtok_view(std::wstring_view const& tokens, wchar_t const delim, bool const ignore_empty = true) {
339 	return strtok_view(tokens, std::wstring_view(&delim, 1), ignore_empty);
340 }
341 
342 /// \private
343 template<typename T, typename String>
344 T to_integral_impl(String const& s, T const errorval = T())
345 {
346 	if constexpr (std::is_same_v<T, bool>) {
347 		return static_cast<T>(to_integral_impl<unsigned int>(s, static_cast<unsigned int>(errorval))) != 0;
348 	}
349 	else if constexpr (std::is_enum_v<T>) {
350 		return static_cast<T>(to_integral_impl<std::underlying_type_t<T>>(s, static_cast<std::underlying_type_t<T>>(errorval)));
351 	}
352 	else {
353 		T ret{};
354 		auto it = s.cbegin();
355 		if (it != s.cend() && (*it == '-' || *it == '+')) {
356 			++it;
357 		}
358 
359 		if (it == s.cend()) {
360 			return errorval;
361 		}
362 
363 		for (; it != s.cend(); ++it) {
364 			auto const& c = *it;
365 			if (c < '0' || c > '9') {
366 				return errorval;
367 			}
368 			ret *= 10;
369 			ret += c - '0';
370 		}
371 
372 		if (!s.empty() && s.front() == '-') {
373 			ret *= static_cast<T>(-1);
374 		}
375 		return ret;
376 	}
377 }
378 
379 /// Converts string to integral type T. If string is not convertible, errorval is returned.
380 template<typename T>
to_integral(std::string_view const & s,T const errorval=T ())381 T to_integral(std::string_view const& s, T const errorval = T()) {
382 	return to_integral_impl<T>(s, errorval);
383 }
384 
385 template<typename T>
to_integral(std::wstring_view const & s,T const errorval=T ())386 T to_integral(std::wstring_view const& s, T const errorval = T()) {
387 	return to_integral_impl<T>(s, errorval);
388 }
389 
390 template<typename T, typename StringType>
391 T to_integral(std::basic_string_view<StringType> const& s, T const errorval = T()) {
392 	return to_integral_impl<T>(s, errorval);
393 }
394 
395 
396 /// \brief Returns true iff the string only has characters in the 7-bit ASCII range
397 template<typename String>
str_is_ascii(String const & s)398 bool str_is_ascii(String const& s) {
399 	for (auto const& c : s) {
400 		if (static_cast<std::make_unsigned_t<typename String::value_type>>(c) > 127) {
401 			return false;
402 		}
403 	}
404 
405 	return true;
406 }
407 
408 /// \private
409 template<typename String, typename Chars>
trim_impl(String & s,Chars const & chars,bool fromLeft,bool fromRight)410 void trim_impl(String & s, Chars const& chars, bool fromLeft, bool fromRight) {
411 	size_t const first = fromLeft ? s.find_first_not_of(chars) : 0;
412 	if (first == String::npos) {
413 		s = String();
414 		return;
415 	}
416 
417 	size_t const last = fromRight ? s.find_last_not_of(chars) : s.size();
418 	if (last == String::npos) {
419 		s = String();
420 		return;
421 	}
422 
423 	// Invariant: If first exists, then last >= first
424 	s = s.substr(first, last - first + 1);
425 }
426 
427 /// \brief Return passed string with all leading and trailing whitespace removed
trimmed(std::string_view s,std::string_view const & chars=" \\r\\n\\t",bool fromLeft=true,bool fromRight=true)428 inline std::string FZ_PUBLIC_SYMBOL trimmed(std::string_view s, std::string_view const& chars = " \r\n\t", bool fromLeft = true, bool fromRight = true)
429 {
430 	trim_impl(s, chars, fromLeft, fromRight);
431 	return std::string(s);
432 }
433 
trimmed(std::wstring_view s,std::wstring_view const & chars=L" \\r\\n\\t",bool fromLeft=true,bool fromRight=true)434 inline std::wstring FZ_PUBLIC_SYMBOL trimmed(std::wstring_view s, std::wstring_view const& chars = L" \r\n\t", bool fromLeft = true, bool fromRight = true)
435 {
436 	trim_impl(s, chars, fromLeft, fromRight);
437 	return std::wstring(s);
438 }
439 
ltrimmed(std::string_view s,std::string_view const & chars=" \\r\\n\\t")440 inline std::string FZ_PUBLIC_SYMBOL ltrimmed(std::string_view s, std::string_view const& chars = " \r\n\t")
441 {
442 	trim_impl(s, chars, true, false);
443 	return std::string(s);
444 }
445 
ltrimmed(std::wstring_view s,std::wstring_view const & chars=L" \\r\\n\\t")446 inline std::wstring FZ_PUBLIC_SYMBOL ltrimmed(std::wstring_view s, std::wstring_view const& chars = L" \r\n\t")
447 {
448 	trim_impl(s, chars, true, false);
449 	return std::wstring(s);
450 }
451 
rtrimmed(std::string_view s,std::string_view const & chars=" \\r\\n\\t")452 inline std::string FZ_PUBLIC_SYMBOL rtrimmed(std::string_view s, std::string_view const& chars = " \r\n\t")
453 {
454 	trim_impl(s, chars, false, true);
455 	return std::string(s);
456 }
457 
rtrimmed(std::wstring_view s,std::wstring_view const & chars=L" \\r\\n\\t")458 inline std::wstring FZ_PUBLIC_SYMBOL rtrimmed(std::wstring_view s, std::wstring_view const& chars = L" \r\n\t")
459 {
460 	trim_impl(s, chars, false, true);
461 	return std::wstring(s);
462 }
463 
464 
465 /// \brief Remove all leading and trailing whitespace from string
466 template<typename String, typename std::enable_if_t<std::is_same_v<typename String::value_type, char>, int> = 0>
trim(String & s,std::string_view const & chars=" \\r\\n\\t",bool fromLeft=true,bool fromRight=true)467 inline void trim(String & s, std::string_view const& chars = " \r\n\t", bool fromLeft = true, bool fromRight = true)
468 {
469 	trim_impl(s, chars, fromLeft, fromRight);
470 }
471 
472 template<typename String, typename std::enable_if_t<std::is_same_v<typename String::value_type, wchar_t>, int> = 0>
trim(String & s,std::wstring_view const & chars=L" \\r\\n\\t",bool fromLeft=true,bool fromRight=true)473 inline void trim(String & s, std::wstring_view const& chars = L" \r\n\t", bool fromLeft = true, bool fromRight = true)
474 {
475 	trim_impl(s, chars, fromLeft, fromRight);
476 }
477 
478 template<typename String, typename std::enable_if_t<std::is_same_v<typename String::value_type, char>, int> = 0>
ltrim(String & s,std::string_view const & chars=" \\r\\n\\t")479 inline void ltrim(String& s, std::string_view const& chars = " \r\n\t")
480 {
481 	trim_impl(s, chars, true, false);
482 }
483 
484 template<typename String, typename std::enable_if_t<std::is_same_v<typename String::value_type, wchar_t>, int> = 0>
ltrim(String & s,std::wstring_view const & chars=L" \\r\\n\\t")485 inline void ltrim(String& s, std::wstring_view  const& chars = L" \r\n\t")
486 {
487 	trim_impl(s, chars, true, false);
488 }
489 
490 template<typename String, typename std::enable_if_t<std::is_same_v<typename String::value_type, char>, int> = 0>
rtrim(String & s,std::string_view const & chars=" \\r\\n\\t")491 inline void rtrim(String& s, std::string_view const& chars = " \r\n\t")
492 {
493 	trim_impl(s, chars, false, true);
494 }
495 
496 template<typename String, typename std::enable_if_t<std::is_same_v<typename String::value_type, wchar_t>, int> = 0>
rtrim(String & s,std::wstring_view const & chars=L" \\r\\n\\t")497 inline void rtrim(String & s, std::wstring_view const& chars = L" \r\n\t")
498 {
499 	trim_impl(s, chars, false, true);
500 }
501 
502 /** \brief Tests whether the first string starts with the second string
503  *
504  * \tparam insensitive_ascii If true, comparison is case-insensitive
505  */
506 template<bool insensitive_ascii = false, typename String>
starts_with(String const & s,String const & beginning)507 bool starts_with(String const& s, String const& beginning)
508 {
509 	if (beginning.size() > s.size()) {
510 		return false;
511 	}
512 	if constexpr (insensitive_ascii) {
513 		return std::equal(beginning.begin(), beginning.end(), s.begin(), [](typename String::value_type const& a, typename String::value_type const& b) {
514 			return tolower_ascii(a) == tolower_ascii(b);
515 		});
516 	}
517 	else {
518 		return std::equal(beginning.begin(), beginning.end(), s.begin());
519 	}
520 }
521 
522 /** \brief Tests whether the first string ends with the second string
523  *
524  * \tparam insensitive_ascii If true, comparison is case-insensitive
525  */
526 template<bool insensitive_ascii = false, typename String>
ends_with(String const & s,String const & ending)527 bool ends_with(String const& s, String const& ending)
528 {
529 	if (ending.size() > s.size()) {
530 		return false;
531 	}
532 
533 	if constexpr (insensitive_ascii) {
534 		return std::equal(ending.rbegin(), ending.rend(), s.rbegin(), [](typename String::value_type const& a, typename String::value_type const& b) {
535 			return tolower_ascii(a) == tolower_ascii(b);
536 		});
537 	}
538 	else {
539 		return std::equal(ending.rbegin(), ending.rend(), s.rbegin());
540 	}
541 }
542 
543 /**
544  * Normalizes various hyphens, dashes and minuses to just hyphen-minus.
545  *
546  * The narrow version assumes UTF-8 as encoding.
547  */
548 std::string FZ_PUBLIC_SYMBOL normalize_hyphens(std::string_view const& in);
549 std::wstring FZ_PUBLIC_SYMBOL normalize_hyphens(std::wstring_view const& in);
550 
551 }
552 
553 #endif
554