1 // Copyright 2020 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #ifndef BASE_STRINGS_STRING_UTIL_IMPL_HELPERS_H_
6 #define BASE_STRINGS_STRING_UTIL_IMPL_HELPERS_H_
7
8 #include <algorithm>
9
10 #include "base/check.h"
11 #include "base/check_op.h"
12 #include "base/logging.h"
13 #include "base/notreached.h"
14 #include "base/ranges/algorithm.h"
15 #include "base/strings/string_piece.h"
16 #include "base/third_party/icu/icu_utf.h"
17 #include "third_party/abseil-cpp/absl/types/optional.h"
18
19 namespace base::internal {
20
21 // Used by ReplaceStringPlaceholders to track the position in the string of
22 // replaced parameters.
23 struct ReplacementOffset {
ReplacementOffsetReplacementOffset24 ReplacementOffset(uintptr_t parameter, size_t offset)
25 : parameter(parameter), offset(offset) {}
26
27 // Index of the parameter.
28 size_t parameter;
29
30 // Starting position in the string.
31 size_t offset;
32 };
33
CompareParameter(const ReplacementOffset & elem1,const ReplacementOffset & elem2)34 static bool CompareParameter(const ReplacementOffset& elem1,
35 const ReplacementOffset& elem2) {
36 return elem1.parameter < elem2.parameter;
37 }
38
39 // Assuming that a pointer is the size of a "machine word", then
40 // uintptr_t is an integer type that is also a machine word.
41 using MachineWord = uintptr_t;
42
IsMachineWordAligned(const void * pointer)43 inline bool IsMachineWordAligned(const void* pointer) {
44 return !(reinterpret_cast<MachineWord>(pointer) & (sizeof(MachineWord) - 1));
45 }
46
47 template <typename T, typename CharT = typename T::value_type>
ToLowerASCIIImpl(T str)48 std::basic_string<CharT> ToLowerASCIIImpl(T str) {
49 std::basic_string<CharT> ret;
50 ret.reserve(str.size());
51 for (size_t i = 0; i < str.size(); i++)
52 ret.push_back(ToLowerASCII(str[i]));
53 return ret;
54 }
55
56 template <typename T, typename CharT = typename T::value_type>
ToUpperASCIIImpl(T str)57 std::basic_string<CharT> ToUpperASCIIImpl(T str) {
58 std::basic_string<CharT> ret;
59 ret.reserve(str.size());
60 for (size_t i = 0; i < str.size(); i++)
61 ret.push_back(ToUpperASCII(str[i]));
62 return ret;
63 }
64
65 template <typename T, typename CharT = typename T::value_type>
TrimStringT(T input,T trim_chars,TrimPositions positions,std::basic_string<CharT> * output)66 TrimPositions TrimStringT(T input,
67 T trim_chars,
68 TrimPositions positions,
69 std::basic_string<CharT>* output) {
70 // Find the edges of leading/trailing whitespace as desired. Need to use
71 // a StringPiece version of input to be able to call find* on it with the
72 // StringPiece version of trim_chars (normally the trim_chars will be a
73 // constant so avoid making a copy).
74 const size_t last_char = input.length() - 1;
75 const size_t first_good_char =
76 (positions & TRIM_LEADING) ? input.find_first_not_of(trim_chars) : 0;
77 const size_t last_good_char = (positions & TRIM_TRAILING)
78 ? input.find_last_not_of(trim_chars)
79 : last_char;
80
81 // When the string was all trimmed, report that we stripped off characters
82 // from whichever position the caller was interested in. For empty input, we
83 // stripped no characters, but we still need to clear |output|.
84 if (input.empty() || first_good_char == std::basic_string<CharT>::npos ||
85 last_good_char == std::basic_string<CharT>::npos) {
86 bool input_was_empty = input.empty(); // in case output == &input
87 output->clear();
88 return input_was_empty ? TRIM_NONE : positions;
89 }
90
91 // Trim.
92 output->assign(input.data() + first_good_char,
93 last_good_char - first_good_char + 1);
94
95 // Return where we trimmed from.
96 return static_cast<TrimPositions>(
97 (first_good_char == 0 ? TRIM_NONE : TRIM_LEADING) |
98 (last_good_char == last_char ? TRIM_NONE : TRIM_TRAILING));
99 }
100
101 template <typename T, typename CharT = typename T::value_type>
TrimStringPieceT(T input,T trim_chars,TrimPositions positions)102 T TrimStringPieceT(T input, T trim_chars, TrimPositions positions) {
103 size_t begin =
104 (positions & TRIM_LEADING) ? input.find_first_not_of(trim_chars) : 0;
105 size_t end = (positions & TRIM_TRAILING)
106 ? input.find_last_not_of(trim_chars) + 1
107 : input.size();
108 return input.substr(std::min(begin, input.size()), end - begin);
109 }
110
111 template <typename T, typename CharT = typename T::value_type>
CollapseWhitespaceT(T text,bool trim_sequences_with_line_breaks)112 std::basic_string<CharT> CollapseWhitespaceT(
113 T text,
114 bool trim_sequences_with_line_breaks) {
115 std::basic_string<CharT> result;
116 result.resize(text.size());
117
118 // Set flags to pretend we're already in a trimmed whitespace sequence, so we
119 // will trim any leading whitespace.
120 bool in_whitespace = true;
121 bool already_trimmed = true;
122
123 size_t chars_written = 0;
124 for (auto c : text) {
125 if (IsWhitespace(c)) {
126 if (!in_whitespace) {
127 // Reduce all whitespace sequences to a single space.
128 in_whitespace = true;
129 result[chars_written++] = L' ';
130 }
131 if (trim_sequences_with_line_breaks && !already_trimmed &&
132 ((c == '\n') || (c == '\r'))) {
133 // Whitespace sequences containing CR or LF are eliminated entirely.
134 already_trimmed = true;
135 --chars_written;
136 }
137 } else {
138 // Non-whitespace characters are copied straight across.
139 in_whitespace = false;
140 already_trimmed = false;
141 result[chars_written++] = c;
142 }
143 }
144
145 if (in_whitespace && !already_trimmed) {
146 // Any trailing whitespace is eliminated.
147 --chars_written;
148 }
149
150 result.resize(chars_written);
151 return result;
152 }
153
154 template <class Char>
DoIsStringASCII(const Char * characters,size_t length)155 bool DoIsStringASCII(const Char* characters, size_t length) {
156 // Bitmasks to detect non ASCII characters for character sizes of 8, 16 and 32
157 // bits.
158 constexpr MachineWord NonASCIIMasks[] = {
159 0, MachineWord(0x8080808080808080ULL), MachineWord(0xFF80FF80FF80FF80ULL),
160 0, MachineWord(0xFFFFFF80FFFFFF80ULL),
161 };
162
163 if (!length)
164 return true;
165 constexpr MachineWord non_ascii_bit_mask = NonASCIIMasks[sizeof(Char)];
166 static_assert(non_ascii_bit_mask, "Error: Invalid Mask");
167 MachineWord all_char_bits = 0;
168 const Char* end = characters + length;
169
170 // Prologue: align the input.
171 while (!IsMachineWordAligned(characters) && characters < end)
172 all_char_bits |= static_cast<MachineWord>(*characters++);
173 if (all_char_bits & non_ascii_bit_mask)
174 return false;
175
176 // Compare the values of CPU word size.
177 constexpr size_t chars_per_word = sizeof(MachineWord) / sizeof(Char);
178 constexpr int batch_count = 16;
179 while (characters <= end - batch_count * chars_per_word) {
180 all_char_bits = 0;
181 for (int i = 0; i < batch_count; ++i) {
182 all_char_bits |= *(reinterpret_cast<const MachineWord*>(characters));
183 characters += chars_per_word;
184 }
185 if (all_char_bits & non_ascii_bit_mask)
186 return false;
187 }
188
189 // Process the remaining words.
190 all_char_bits = 0;
191 while (characters <= end - chars_per_word) {
192 all_char_bits |= *(reinterpret_cast<const MachineWord*>(characters));
193 characters += chars_per_word;
194 }
195
196 // Process the remaining bytes.
197 while (characters < end)
198 all_char_bits |= static_cast<MachineWord>(*characters++);
199
200 return !(all_char_bits & non_ascii_bit_mask);
201 }
202
203 template <bool (*Validator)(base_icu::UChar32)>
DoIsStringUTF8(StringPiece str)204 inline bool DoIsStringUTF8(StringPiece str) {
205 const uint8_t* src = reinterpret_cast<const uint8_t*>(str.data());
206 size_t src_len = str.length();
207 size_t char_index = 0;
208
209 while (char_index < src_len) {
210 base_icu::UChar32 code_point;
211 CBU8_NEXT(src, char_index, src_len, code_point);
212 if (!Validator(code_point))
213 return false;
214 }
215 return true;
216 }
217
218 template <typename T, typename CharT = typename T::value_type>
StartsWithT(T str,T search_for,CompareCase case_sensitivity)219 bool StartsWithT(T str, T search_for, CompareCase case_sensitivity) {
220 if (search_for.size() > str.size())
221 return false;
222
223 BasicStringPiece<CharT> source = str.substr(0, search_for.size());
224
225 switch (case_sensitivity) {
226 case CompareCase::SENSITIVE:
227 return source == search_for;
228
229 case CompareCase::INSENSITIVE_ASCII:
230 return std::equal(search_for.begin(), search_for.end(), source.begin(),
231 CaseInsensitiveCompareASCII<CharT>());
232 }
233 }
234
235 template <typename T, typename CharT = typename T::value_type>
EndsWithT(T str,T search_for,CompareCase case_sensitivity)236 bool EndsWithT(T str, T search_for, CompareCase case_sensitivity) {
237 if (search_for.size() > str.size())
238 return false;
239
240 BasicStringPiece<CharT> source =
241 str.substr(str.size() - search_for.size(), search_for.size());
242
243 switch (case_sensitivity) {
244 case CompareCase::SENSITIVE:
245 return source == search_for;
246
247 case CompareCase::INSENSITIVE_ASCII:
248 return std::equal(source.begin(), source.end(), search_for.begin(),
249 CaseInsensitiveCompareASCII<CharT>());
250 }
251 }
252
253 // A Matcher for DoReplaceMatchesAfterOffset() that matches substrings.
254 template <class CharT>
255 struct SubstringMatcher {
256 BasicStringPiece<CharT> find_this;
257
FindSubstringMatcher258 size_t Find(const std::basic_string<CharT>& input, size_t pos) {
259 return input.find(find_this.data(), pos, find_this.length());
260 }
MatchSizeSubstringMatcher261 size_t MatchSize() { return find_this.length(); }
262 };
263
264 // Type deduction helper for SubstringMatcher.
265 template <typename T, typename CharT = typename T::value_type>
MakeSubstringMatcher(T find_this)266 auto MakeSubstringMatcher(T find_this) {
267 return SubstringMatcher<CharT>{find_this};
268 }
269
270 // A Matcher for DoReplaceMatchesAfterOffset() that matches single characters.
271 template <class CharT>
272 struct CharacterMatcher {
273 BasicStringPiece<CharT> find_any_of_these;
274
FindCharacterMatcher275 size_t Find(const std::basic_string<CharT>& input, size_t pos) {
276 return input.find_first_of(find_any_of_these.data(), pos,
277 find_any_of_these.length());
278 }
MatchSizeCharacterMatcher279 constexpr size_t MatchSize() { return 1; }
280 };
281
282 // Type deduction helper for CharacterMatcher.
283 template <typename T, typename CharT = typename T::value_type>
MakeCharacterMatcher(T find_any_of_these)284 auto MakeCharacterMatcher(T find_any_of_these) {
285 return CharacterMatcher<CharT>{find_any_of_these};
286 }
287
288 enum class ReplaceType { REPLACE_ALL, REPLACE_FIRST };
289
290 // Runs in O(n) time in the length of |str|, and transforms the string without
291 // reallocating when possible. Returns |true| if any matches were found.
292 //
293 // This is parameterized on a |Matcher| traits type, so that it can be the
294 // implementation for both ReplaceChars() and ReplaceSubstringsAfterOffset().
295 template <typename Matcher, typename T, typename CharT = typename T::value_type>
DoReplaceMatchesAfterOffset(std::basic_string<CharT> * str,size_t initial_offset,Matcher matcher,T replace_with,ReplaceType replace_type)296 bool DoReplaceMatchesAfterOffset(std::basic_string<CharT>* str,
297 size_t initial_offset,
298 Matcher matcher,
299 T replace_with,
300 ReplaceType replace_type) {
301 using CharTraits = std::char_traits<CharT>;
302
303 const size_t find_length = matcher.MatchSize();
304 if (!find_length)
305 return false;
306
307 // If the find string doesn't appear, there's nothing to do.
308 size_t first_match = matcher.Find(*str, initial_offset);
309 if (first_match == std::basic_string<CharT>::npos)
310 return false;
311
312 // If we're only replacing one instance, there's no need to do anything
313 // complicated.
314 const size_t replace_length = replace_with.length();
315 if (replace_type == ReplaceType::REPLACE_FIRST) {
316 str->replace(first_match, find_length, replace_with.data(), replace_length);
317 return true;
318 }
319
320 // If the find and replace strings are the same length, we can simply use
321 // replace() on each instance, and finish the entire operation in O(n) time.
322 if (find_length == replace_length) {
323 auto* buffer = &((*str)[0]);
324 for (size_t offset = first_match; offset != std::basic_string<CharT>::npos;
325 offset = matcher.Find(*str, offset + replace_length)) {
326 CharTraits::copy(buffer + offset, replace_with.data(), replace_length);
327 }
328 return true;
329 }
330
331 // Since the find and replace strings aren't the same length, a loop like the
332 // one above would be O(n^2) in the worst case, as replace() will shift the
333 // entire remaining string each time. We need to be more clever to keep things
334 // O(n).
335 //
336 // When the string is being shortened, it's possible to just shift the matches
337 // down in one pass while finding, and truncate the length at the end of the
338 // search.
339 //
340 // If the string is being lengthened, more work is required. The strategy used
341 // here is to make two find() passes through the string. The first pass counts
342 // the number of matches to determine the new size. The second pass will
343 // either construct the new string into a new buffer (if the existing buffer
344 // lacked capacity), or else -- if there is room -- create a region of scratch
345 // space after |first_match| by shifting the tail of the string to a higher
346 // index, and doing in-place moves from the tail to lower indices thereafter.
347 size_t str_length = str->length();
348 size_t expansion = 0;
349 if (replace_length > find_length) {
350 // This operation lengthens the string; determine the new length by counting
351 // matches.
352 const size_t expansion_per_match = (replace_length - find_length);
353 size_t num_matches = 0;
354 for (size_t match = first_match; match != std::basic_string<CharT>::npos;
355 match = matcher.Find(*str, match + find_length)) {
356 expansion += expansion_per_match;
357 ++num_matches;
358 }
359 const size_t final_length = str_length + expansion;
360
361 if (str->capacity() < final_length) {
362 // If we'd have to allocate a new buffer to grow the string, build the
363 // result directly into the new allocation via append().
364 std::basic_string<CharT> src(str->get_allocator());
365 str->swap(src);
366 str->reserve(final_length);
367
368 size_t pos = 0;
369 for (size_t match = first_match;; match = matcher.Find(src, pos)) {
370 str->append(src, pos, match - pos);
371 str->append(replace_with.data(), replace_length);
372 pos = match + find_length;
373
374 // A mid-loop test/break enables skipping the final Find() call; the
375 // number of matches is known, so don't search past the last one.
376 if (!--num_matches)
377 break;
378 }
379
380 // Handle substring after the final match.
381 str->append(src, pos, str_length - pos);
382 return true;
383 }
384
385 // Prepare for the copy/move loop below -- expand the string to its final
386 // size by shifting the data after the first match to the end of the resized
387 // string.
388 size_t shift_src = first_match + find_length;
389 size_t shift_dst = shift_src + expansion;
390
391 // Big |expansion| factors (relative to |str_length|) require padding up to
392 // |shift_dst|.
393 if (shift_dst > str_length)
394 str->resize(shift_dst);
395
396 str->replace(shift_dst, str_length - shift_src, *str, shift_src,
397 str_length - shift_src);
398 str_length = final_length;
399 }
400
401 // We can alternate replacement and move operations. This won't overwrite the
402 // unsearched region of the string so long as |write_offset| <= |read_offset|;
403 // that condition is always satisfied because:
404 //
405 // (a) If the string is being shortened, |expansion| is zero and
406 // |write_offset| grows slower than |read_offset|.
407 //
408 // (b) If the string is being lengthened, |write_offset| grows faster than
409 // |read_offset|, but |expansion| is big enough so that |write_offset|
410 // will only catch up to |read_offset| at the point of the last match.
411 auto* buffer = &((*str)[0]);
412 size_t write_offset = first_match;
413 size_t read_offset = first_match + expansion;
414 do {
415 if (replace_length) {
416 CharTraits::copy(buffer + write_offset, replace_with.data(),
417 replace_length);
418 write_offset += replace_length;
419 }
420 read_offset += find_length;
421
422 // min() clamps std::basic_string<CharT>::npos (the largest unsigned value)
423 // to str_length.
424 size_t match = std::min(matcher.Find(*str, read_offset), str_length);
425
426 size_t length = match - read_offset;
427 if (length) {
428 CharTraits::move(buffer + write_offset, buffer + read_offset, length);
429 write_offset += length;
430 read_offset += length;
431 }
432 } while (read_offset < str_length);
433
434 // If we're shortening the string, truncate it now.
435 str->resize(write_offset);
436 return true;
437 }
438
439 template <typename T, typename CharT = typename T::value_type>
ReplaceCharsT(T input,T find_any_of_these,T replace_with,std::basic_string<CharT> * output)440 bool ReplaceCharsT(T input,
441 T find_any_of_these,
442 T replace_with,
443 std::basic_string<CharT>* output) {
444 // Commonly, this is called with output and input being the same string; in
445 // that case, skip the copy.
446 if (input.data() != output->data() || input.size() != output->size())
447 output->assign(input.data(), input.size());
448
449 return DoReplaceMatchesAfterOffset(output, 0,
450 MakeCharacterMatcher(find_any_of_these),
451 replace_with, ReplaceType::REPLACE_ALL);
452 }
453
454 template <class string_type>
WriteIntoT(string_type * str,size_t length_with_null)455 inline typename string_type::value_type* WriteIntoT(string_type* str,
456 size_t length_with_null) {
457 DCHECK_GE(length_with_null, 1u);
458 str->reserve(length_with_null);
459 str->resize(length_with_null - 1);
460 return &((*str)[0]);
461 }
462
463 // Generic version for all JoinString overloads. |list_type| must be a sequence
464 // (base::span or std::initializer_list) of strings/StringPieces (std::string,
465 // std::u16string, StringPiece or StringPiece16). |CharT| is either char or
466 // char16_t.
467 template <typename list_type,
468 typename T,
469 typename CharT = typename T::value_type>
JoinStringT(list_type parts,T sep)470 static std::basic_string<CharT> JoinStringT(list_type parts, T sep) {
471 if (std::empty(parts))
472 return std::basic_string<CharT>();
473
474 // Pre-allocate the eventual size of the string. Start with the size of all of
475 // the separators (note that this *assumes* parts.size() > 0).
476 size_t total_size = (parts.size() - 1) * sep.size();
477 for (const auto& part : parts)
478 total_size += part.size();
479 std::basic_string<CharT> result;
480 result.reserve(total_size);
481
482 auto iter = parts.begin();
483 DCHECK(iter != parts.end());
484 result.append(iter->data(), iter->size());
485 ++iter;
486
487 for (; iter != parts.end(); ++iter) {
488 result.append(sep.data(), sep.size());
489 result.append(iter->data(), iter->size());
490 }
491
492 // Sanity-check that we pre-allocated correctly.
493 DCHECK_EQ(total_size, result.size());
494
495 return result;
496 }
497
498 // Replaces placeholders in `format_string` with values from `subst`.
499 // * `placeholder_prefix`: Allows using a specific character as the placeholder
500 // prefix. `base::ReplaceStringPlaceholders` uses '$'.
501 // * `should_escape_multiple_placeholder_prefixes`:
502 // * If this parameter is `true`, which is the case with
503 // `base::ReplaceStringPlaceholders`, `placeholder_prefix` characters are
504 // replaced by that number less one. Eg $$->$, $$$->$$, etc.
505 // * If this parameter is `false`, each literal `placeholder_prefix` character
506 // in `format_string` is escaped with another `placeholder_prefix`. For
507 // instance, with `%` as the `placeholder_prefix`: %%->%, %%%%->%%, etc.
508 // * `is_strict_mode`:
509 // * If this parameter is `true`, error handling is stricter. The function
510 // returns `absl::nullopt` if:
511 // * a placeholder %N is encountered where N > substitutions.size().
512 // * a literal `%` is not escaped with a `%`.
513 template <typename T, typename CharT = typename T::value_type>
DoReplaceStringPlaceholders(T format_string,const std::vector<std::basic_string<CharT>> & subst,const CharT placeholder_prefix,const bool should_escape_multiple_placeholder_prefixes,const bool is_strict_mode,std::vector<size_t> * offsets)514 absl::optional<std::basic_string<CharT>> DoReplaceStringPlaceholders(
515 T format_string,
516 const std::vector<std::basic_string<CharT>>& subst,
517 const CharT placeholder_prefix,
518 const bool should_escape_multiple_placeholder_prefixes,
519 const bool is_strict_mode,
520 std::vector<size_t>* offsets) {
521 size_t substitutions = subst.size();
522 DCHECK_LT(substitutions, 11U);
523
524 size_t sub_length = 0;
525 for (const auto& cur : subst) {
526 sub_length += cur.length();
527 }
528
529 std::basic_string<CharT> formatted;
530 formatted.reserve(format_string.length() + sub_length);
531
532 std::vector<ReplacementOffset> r_offsets;
533 for (auto i = format_string.begin(); i != format_string.end(); ++i) {
534 if (placeholder_prefix == *i) {
535 if (i + 1 != format_string.end()) {
536 ++i;
537 if (placeholder_prefix == *i) {
538 do {
539 formatted.push_back(placeholder_prefix);
540 ++i;
541 } while (should_escape_multiple_placeholder_prefixes &&
542 i != format_string.end() && placeholder_prefix == *i);
543 --i;
544 } else {
545 if (*i < '1' || *i > '9') {
546 if (is_strict_mode) {
547 DLOG(ERROR) << "Invalid placeholder after placeholder prefix: "
548 << std::basic_string<CharT>(1, placeholder_prefix)
549 << std::basic_string<CharT>(1, *i);
550 return absl::nullopt;
551 }
552
553 continue;
554 }
555 const size_t index = static_cast<size_t>(*i - '1');
556 if (offsets) {
557 ReplacementOffset r_offset(index, formatted.size());
558 r_offsets.insert(
559 ranges::upper_bound(r_offsets, r_offset, &CompareParameter),
560 r_offset);
561 }
562 if (index < substitutions) {
563 formatted.append(subst.at(index));
564 } else if (is_strict_mode) {
565 DLOG(ERROR) << "index out of range: " << index << ": "
566 << substitutions;
567 return absl::nullopt;
568 }
569 }
570 } else if (is_strict_mode) {
571 DLOG(ERROR) << "unexpected placeholder prefix at end of string";
572 return absl::nullopt;
573 }
574 } else {
575 formatted.push_back(*i);
576 }
577 }
578 if (offsets) {
579 for (const auto& cur : r_offsets) {
580 offsets->push_back(cur.offset);
581 }
582 }
583 return formatted;
584 }
585
586 // The following code is compatible with the OpenBSD lcpy interface. See:
587 // http://www.gratisoft.us/todd/papers/strlcpy.html
588 // ftp://ftp.openbsd.org/pub/OpenBSD/src/lib/libc/string/{wcs,str}lcpy.c
589
590 template <typename CHAR>
lcpyT(CHAR * dst,const CHAR * src,size_t dst_size)591 size_t lcpyT(CHAR* dst, const CHAR* src, size_t dst_size) {
592 for (size_t i = 0; i < dst_size; ++i) {
593 if ((dst[i] = src[i]) == 0) // We hit and copied the terminating NULL.
594 return i;
595 }
596
597 // We were left off at dst_size. We over copied 1 byte. Null terminate.
598 if (dst_size != 0)
599 dst[dst_size - 1] = 0;
600
601 // Count the rest of the |src|, and return it's length in characters.
602 while (src[dst_size])
603 ++dst_size;
604 return dst_size;
605 }
606
607 } // namespace base::internal
608
609 #endif // BASE_STRINGS_STRING_UTIL_IMPL_HELPERS_H_
610