Fix corner case in CStringUtils::GetAccellerator
[TortoiseGit.git] / src / Utils / StringUtils.cpp
blob9fc8fd47100cb5aa0734b334cec2dac592e27ad8
1 // TortoiseGit - a Windows shell extension for easy version control
3 // Copyright (C) 2011-2019, 2021-2023 - TortoiseGit
4 // Copyright (C) 2003-2011, 2015 - TortoiseSVN
6 // This program is free software; you can redistribute it and/or
7 // modify it under the terms of the GNU General Public License
8 // as published by the Free Software Foundation; either version 2
9 // of the License, or (at your option) any later version.
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
16 // You should have received a copy of the GNU General Public License
17 // along with this program; if not, write to the Free Software Foundation,
18 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "stdafx.h"
21 #include "UnicodeUtils.h"
22 #include "StringUtils.h"
23 #include "ClipboardHelper.h"
24 #include "SmartHandle.h"
26 int strwildcmp(const char *wild, const char *string)
28 const char* cp = nullptr;
29 const char* mp = nullptr;
30 while ((*string) && (*wild != '*'))
32 if ((*wild != *string) && (*wild != '?'))
33 return 0;
34 ++wild;
35 ++string;
37 while (*string)
39 if (*wild == '*')
41 if (!*++wild)
42 return 1;
43 mp = wild;
44 cp = string+1;
46 else if ((*wild == *string) || (*wild == '?'))
48 ++wild;
49 ++string;
51 else
53 wild = mp;
54 string = cp++;
58 while (*wild == '*')
59 ++wild;
60 return !*wild;
63 int wcswildcmp(const wchar_t *wild, const wchar_t *string)
65 const wchar_t* cp = nullptr;
66 const wchar_t* mp = nullptr;
67 while ((*string) && (*wild != '*'))
69 if ((*wild != *string) && (*wild != '?'))
70 return 0;
71 ++wild;
72 ++string;
74 while (*string)
76 if (*wild == '*')
78 if (!*++wild)
79 return 1;
80 mp = wild;
81 cp = string+1;
83 else if ((*wild == *string) || (*wild == '?'))
85 ++wild;
86 ++string;
88 else
90 wild = mp;
91 string = cp++;
95 while (*wild == '*')
96 ++wild;
97 return !*wild;
100 #if defined(CSTRING_AVAILABLE) || defined(_MFC_VER)
101 void CStringUtils::RemoveAccelerators(CString& text)
103 int pos = 0;
104 while ((pos=text.Find('&',pos))>=0)
106 if (text.GetLength() > (pos-1))
108 if (text.GetAt(pos+1)!=' ')
109 text.Delete(pos);
111 ++pos;
115 CString CStringUtils::EscapeAccellerators(CString& text)
117 text.Replace(L"&", L"&&");
118 return text;
121 wchar_t CStringUtils::GetAccellerator(const CString& text)
123 int pos = 0;
124 while ((pos = text.Find(L'&', pos)) >= 0 && pos + 1 < text.GetLength())
126 if (text.GetAt(pos + 1) == '&')
127 ++pos;
128 else if (text.GetAt(pos + 1) != ' ')
129 return towupper(text.GetAt(pos + 1));
130 ++pos;
132 return L'\0';
134 #endif
135 #ifdef _MFC_VER
136 bool CStringUtils::WriteAsciiStringToClipboard(const CStringA& sClipdata, LCID lcid, HWND hOwningWnd)
138 CClipboardHelper clipboardHelper;
139 if (!clipboardHelper.Open(hOwningWnd))
140 return false;
142 EmptyClipboard();
143 HGLOBAL hClipboardData = CClipboardHelper::GlobalAlloc(sClipdata.GetLength() + 1);
144 if (!hClipboardData)
145 return false;
147 auto pchData = static_cast<char*>(GlobalLock(hClipboardData));
148 if (!pchData)
149 return false;
151 strcpy_s(pchData, sClipdata.GetLength() + 1, static_cast<LPCSTR>(sClipdata));
152 GlobalUnlock(hClipboardData);
153 if (!SetClipboardData(CF_TEXT, hClipboardData))
154 return false;
156 HANDLE hlocmem = CClipboardHelper::GlobalAlloc(sizeof(LCID));
157 if (!hlocmem)
158 return false;
160 auto plcid = static_cast<PLCID>(GlobalLock(hlocmem));
161 if (plcid)
163 *plcid = lcid;
164 SetClipboardData(CF_LOCALE, static_cast<HANDLE>(plcid));
166 GlobalUnlock(hlocmem);
168 return true;
171 bool CStringUtils::WriteAsciiStringToClipboard(const CStringW& sClipdata, HWND hOwningWnd)
173 CClipboardHelper clipboardHelper;
174 if (!clipboardHelper.Open(hOwningWnd))
175 return false;
177 EmptyClipboard();
178 HGLOBAL hClipboardData = CClipboardHelper::GlobalAlloc((sClipdata.GetLength() + 1) * sizeof(WCHAR));
179 if (!hClipboardData)
180 return false;
182 auto pchData = static_cast<WCHAR*>(GlobalLock(hClipboardData));
183 if (!pchData)
184 return false;
186 wcscpy_s(pchData, sClipdata.GetLength() + 1, static_cast<LPCWSTR>(sClipdata));
187 GlobalUnlock(hClipboardData);
188 if (!SetClipboardData(CF_UNICODETEXT, hClipboardData))
189 return false;
191 // no need to also set CF_TEXT : the OS does this
192 // automatically.
193 return true;
196 bool CStringUtils::WriteDiffToClipboard(const CStringA& sClipdata, HWND hOwningWnd)
198 UINT cFormat = RegisterClipboardFormat(L"TGIT_UNIFIEDDIFF");
199 if (cFormat == 0)
200 return false;
201 CClipboardHelper clipboardHelper;
202 if (!clipboardHelper.Open(hOwningWnd))
203 return false;
205 EmptyClipboard();
206 HGLOBAL hClipboardData = CClipboardHelper::GlobalAlloc(sClipdata.GetLength() + 1);
207 if (!hClipboardData)
208 return false;
210 auto pchData = static_cast<char*>(GlobalLock(hClipboardData));
211 if (!pchData)
212 return false;
214 strcpy_s(pchData, sClipdata.GetLength() + 1, static_cast<LPCSTR>(sClipdata));
215 GlobalUnlock(hClipboardData);
216 if (!SetClipboardData(cFormat,hClipboardData))
217 return false;
218 if (!SetClipboardData(CF_TEXT, hClipboardData))
219 return false;
221 CString sClipdataW = CUnicodeUtils::GetUnicode(sClipdata);
222 auto hClipboardDataW = CClipboardHelper::GlobalAlloc((sClipdataW.GetLength() + 1) * sizeof(wchar_t));
223 if (!hClipboardDataW)
224 return false;
226 auto pchDataW = static_cast<wchar_t*>(GlobalLock(hClipboardDataW));
227 if (!pchDataW)
228 return false;
230 wcscpy_s(pchDataW, sClipdataW.GetLength() + 1, static_cast<LPCWSTR>(sClipdataW));
231 GlobalUnlock(hClipboardDataW);
232 if (!SetClipboardData(CF_UNICODETEXT, hClipboardDataW))
233 return false;
235 return true;
237 #endif
239 #ifdef _MFC_VER
240 bool CStringUtils::ReadStringFromTextFile(const CString& path, CString& text)
242 if (!PathFileExists(path))
243 return false;
246 CStdioFile file;
247 // w/o typeBinary for some files \r gets dropped
248 if (!file.Open(path, CFile::typeBinary | CFile::modeRead | CFile::shareDenyWrite) || file.GetLength() >= INT_MAX)
249 return false;
251 CStringA filecontent;
252 const UINT filelength = static_cast<UINT>(file.GetLength());
253 const int bytesread = static_cast<int>(file.Read(filecontent.GetBuffer(filelength), filelength));
254 filecontent.ReleaseBuffer(bytesread);
255 text = CUnicodeUtils::GetUnicode(filecontent);
256 file.Close();
258 catch (CFileException* pE)
260 text.Empty();
261 pE->Delete();
262 return false;
264 return true;
266 #endif // #ifdef _MFC_VER
268 #if defined(CSTRING_AVAILABLE) || defined(_MFC_VER)
269 BOOL CStringUtils::WildCardMatch(const CString& wildcard, const CString& string)
271 return wcswildcmp(wildcard, string);
274 CString CStringUtils::LinesWrap(const CString& longstring, int limit /* = 80 */, bool bCompactPaths /* = true */)
276 CString retString;
277 if ((longstring.GetLength() < limit) || (limit == 0))
278 return longstring; // no wrapping needed.
279 // now start breaking the string into lines
281 int linepos = 0;
282 int lineposold = 0;
283 CString temp;
284 while ((linepos = longstring.Find('\n', linepos)) >= 0)
286 temp = longstring.Mid(lineposold, linepos-lineposold);
287 if ((linepos+1)<longstring.GetLength())
288 ++linepos;
289 else
290 break;
291 lineposold = linepos;
292 if (!retString.IsEmpty())
293 retString += L'\n';
294 retString += WordWrap(temp, limit, bCompactPaths, false, 4);
296 temp = longstring.Mid(lineposold);
297 if (!temp.IsEmpty())
298 retString += L'\n';
299 retString += WordWrap(temp, limit, bCompactPaths, false, 4);
300 retString.Trim();
301 return retString;
304 CString CStringUtils::WordWrap(const CString& longstring, int limit, bool bCompactPaths, bool bForceWrap, int tabSize)
306 int nLength = longstring.GetLength();
307 CString retString;
309 if (limit < 0)
310 limit = 0;
312 int nLineStart = 0;
313 int nLineEnd = 0;
314 int tabOffset = 0;
315 for (int i = 0; i < nLength; ++i)
317 if (i-nLineStart+tabOffset >= limit)
319 if (nLineEnd == nLineStart)
321 if (bForceWrap)
322 nLineEnd = i;
323 else
325 while ((i < nLength) && (longstring[i] != ' ') && (longstring[i] != '\t'))
326 ++i;
327 nLineEnd = i;
330 if (bCompactPaths)
332 CString longline = longstring.Mid(nLineStart, nLineEnd-nLineStart).Left(MAX_PATH-1);
333 if ((bCompactPaths)&&(longline.GetLength() < MAX_PATH))
335 if (((!PathIsFileSpec(longline))&&longline.Find(':')<3)||(PathIsURL(longline)))
337 wchar_t buf[MAX_PATH] = { 0 };
338 PathCompactPathEx(buf, longline, limit+1, 0);
339 longline = buf;
342 retString += longline;
344 else
345 retString += longstring.Mid(nLineStart, nLineEnd-nLineStart);
346 retString += L'\n';
347 tabOffset = 0;
348 nLineStart = nLineEnd;
350 if (longstring[i] == ' ')
351 nLineEnd = i;
352 if (longstring[i] == '\t')
354 tabOffset += (tabSize - i % tabSize);
355 nLineEnd = i;
358 if (bCompactPaths)
360 CString longline = longstring.Mid(nLineStart).Left(MAX_PATH-1);
361 if ((bCompactPaths)&&(longline.GetLength() < MAX_PATH))
363 if (((!PathIsFileSpec(longline))&&longline.Find(':')<3)||(PathIsURL(longline)))
365 wchar_t buf[MAX_PATH] = { 0 };
366 PathCompactPathEx(buf, longline, limit+1, 0);
367 longline = buf;
370 retString += longline;
372 else
373 retString += longstring.Mid(nLineStart);
375 return retString;
378 std::vector<CString> CStringUtils::WordWrap(const CString& longstring, int limit, int tabSize)
380 int nLength = longstring.GetLength();
381 std::vector<CString> retVec;
383 if (limit < 0)
384 limit = 0;
386 int nLineStart = 0;
387 int nLineEnd = 0;
388 int tabOffset = 0;
389 for (int i = 0; i < nLength; ++i)
391 if (i - nLineStart + tabOffset >= limit)
393 if (nLineEnd == nLineStart)
394 nLineEnd = i;
396 auto sMid = longstring.Mid(nLineStart, nLineEnd - nLineStart);
397 retVec.push_back(sMid);
399 tabOffset = 0;
400 nLineStart = nLineEnd;
402 if (longstring[i] == ' ')
403 nLineEnd = i;
404 if (longstring[i] == '\t')
406 tabOffset += (tabSize - i % tabSize);
407 nLineEnd = i;
411 auto sMid = longstring.Mid(nLineStart);
412 retVec.push_back(sMid);
414 return retVec;
417 int CStringUtils::GetMatchingLength (const CString& lhs, const CString& rhs)
419 const int lhsLength = lhs.GetLength();
420 const int rhsLength = rhs.GetLength();
421 const int maxResult = min(lhsLength, rhsLength);
423 LPCWSTR pLhs = lhs;
424 LPCWSTR pRhs = rhs;
426 for (int i = 0; i < maxResult; ++i)
427 if (pLhs[i] != pRhs[i])
428 return i;
430 return maxResult;
433 int CStringUtils::FastCompareNoCase (const CStringW& lhs, const CStringW& rhs)
435 // attempt latin-only comparison
437 INT_PTR count = min (lhs.GetLength(), rhs.GetLength()+1);
438 const wchar_t* left = lhs;
439 const wchar_t* right = rhs;
440 for (const wchar_t* last = left + count+1; left < last; ++left, ++right)
442 int leftChar = *left;
443 int rightChar = *right;
445 int diff = leftChar - rightChar;
446 if (diff != 0)
448 // case-sensitive comparison found a difference
450 if ((leftChar | rightChar) >= 0x80)
452 // non-latin char -> fall back to CRT code
453 // (full comparison required as we might have
454 // skipped special chars / UTF plane selectors)
456 return _wcsicmp (lhs, rhs);
459 // normalize to lower case
461 if ((leftChar >= 'A') && (leftChar <= 'Z'))
462 leftChar += 'a' - 'A';
463 if ((rightChar >= 'A') && (rightChar <= 'Z'))
464 rightChar += 'a' - 'A';
466 // compare again
468 diff = leftChar - rightChar;
469 if (diff != 0)
470 return diff;
474 // must be equal (both ended with a 0)
476 return 0;
479 bool CStringUtils::IsPlainReadableASCII(const CString& text)
481 for (int i = 0; i < text.GetLength(); ++i)
483 if (text[i] < 32 || text[i] >= 127)
484 return false;
486 return true;
489 static void cleanup_space(CString& string)
491 for (int pos = 0; pos < string.GetLength(); ++pos)
493 if (_istspace(string[pos])) {
494 string.SetAt(pos, L' ');
495 int cnt;
496 for (cnt = 0; _istspace(string[pos + cnt + 1]); ++cnt);
497 string.Delete(pos + 1, cnt);
502 // similar code in CTortoiseGitBlameData::UnquoteFilename
503 CString CStringUtils::UnescapeGitQuotePath(const CString& s)
505 CStringA t;
506 const int i_size = s.GetLength();
507 bool isEscaped = false;
508 for (int i = 0; i < i_size; ++i)
510 wchar_t c = s[i];
511 if (isEscaped)
513 if (c >= '0' && c <= '3')
515 if (i + 2 < i_size)
517 c = (((c - L'0') & 03) << 6) | (((s[i + 1] - L'0') & 07) << 3) | ((s[i + 2] - L'0') & 07);
518 i += 2;
519 t += c;
522 else
524 // we're on purpose not supporting all possible abbreviations such as \n, \r, \t here as these filenames are invalid on Windows anaway
525 t += c;
527 isEscaped = false;
529 else
531 if (c == L'\\')
532 isEscaped = true;
533 else if (c == L'"')
534 break;
535 else
536 t += c;
539 return CUnicodeUtils::GetUnicode(t);
542 static void get_sane_name(CString* out, const CString* name, const CString& email)
544 const CString* src = name;
545 if (name->GetLength() < 3 || 60 < name->GetLength() || wcschr(*name, L'@') || wcschr(*name, L'<') || wcschr(*name, L'>'))
546 src = &email;
547 else if (name == out)
548 return;
549 *out = *src;
552 static void parse_bogus_from(const CString& mailaddress, CString& parsedAddress, CString* parsedName)
554 /* John Doe <johndoe> */
556 const int bra = mailaddress.Find(L'<');
557 if (bra < 0)
558 return;
559 const int ket = mailaddress.Find(L'>');
560 if (ket < 0)
561 return;
563 parsedAddress = mailaddress.Mid(bra + 1, ket - bra - 1);
565 if (parsedName)
567 *parsedName = mailaddress.Left(bra).Trim();
568 get_sane_name(parsedName, parsedName, parsedAddress);
572 void CStringUtils::ParseEmailAddress(CString mailaddress, CString& parsedAddress, CString* parsedName)
574 if (parsedName)
575 parsedName->Empty();
577 mailaddress = mailaddress.Trim();
578 if (mailaddress.IsEmpty())
580 parsedAddress.Empty();
581 return;
584 if (mailaddress.Left(1) == L'"')
586 mailaddress = mailaddress.TrimLeft();
587 bool escaped = false;
588 bool opened = true;
589 for (int i = 1; i < mailaddress.GetLength(); ++i)
591 if (mailaddress[i] == L'"')
593 if (!escaped)
595 opened = !opened;
596 if (!opened)
598 if (parsedName)
599 *parsedName = mailaddress.Mid(1, i - 1);
600 mailaddress = mailaddress.Mid(i);
601 break;
604 else
606 escaped = false;
607 mailaddress.Delete(i - 1);
608 --i;
611 else if (mailaddress[i] == L'\\')
612 escaped = !escaped;
616 const auto buf = mailaddress.GetBuffer();
617 auto at = wcschr(buf, L'@');
618 if (!at)
620 parse_bogus_from(mailaddress, parsedAddress, parsedName);
621 return;
624 /* Pick up the string around '@', possibly delimited with <>
625 * pair; that is the email part.
627 while (at > buf)
629 auto c = at[-1];
630 if (_istspace(c))
631 break;
632 if (c == L'<')
634 at[-1] = L' ';
635 break;
637 at--;
640 mailaddress.ReleaseBuffer();
641 const size_t el = wcscspn(at, L" \n\t\r\v\f>");
642 parsedAddress = mailaddress.Mid(static_cast<int>(at - buf), static_cast<int>(el));
643 mailaddress.Delete(static_cast<int>(at - buf), static_cast<int>(el + (at[el] ? 1 : 0)));
645 /* The remainder is name. It could be
647 * - "John Doe <john.doe@xz>" (a), or
648 * - "john.doe@xz (John Doe)" (b), or
649 * - "John (zzz) Doe <john.doe@xz> (Comment)" (c)
651 * but we have removed the email part, so
653 * - remove extra spaces which could stay after email (case 'c'), and
654 * - trim from both ends, possibly removing the () pair at the end
655 * (cases 'b' and 'c').
657 cleanup_space(mailaddress);
658 mailaddress.Trim();
659 if (!mailaddress.IsEmpty() && ((mailaddress[0] == L'(' && mailaddress[mailaddress.GetLength() - 1] == L')') || (mailaddress[0] == L'"' && mailaddress[mailaddress.GetLength() - 1] == L'"')))
660 mailaddress = mailaddress.Mid(1, mailaddress.GetLength() - 2);
662 if (parsedName && parsedName->IsEmpty())
663 get_sane_name(parsedName, &mailaddress, parsedAddress);
666 bool CStringUtils::StartsWith(const wchar_t* heystack, const CString& needle)
668 return wcsncmp(heystack, needle, needle.GetLength()) == 0;
671 bool CStringUtils::EndsWith(const CString& heystack, const wchar_t* needle)
673 const auto lenNeedle = wcslen(needle);
674 const auto lenHeystack = static_cast<size_t>(heystack.GetLength());
675 if (lenNeedle > lenHeystack)
676 return false;
677 return wcsncmp(static_cast<LPCWSTR>(heystack) + (lenHeystack - lenNeedle), needle, lenNeedle) == 0;
680 bool CStringUtils::EndsWith(const CString& heystack, const wchar_t needle)
682 const auto lenHeystack = heystack.GetLength();
683 if (!lenHeystack)
684 return false;
685 return *(static_cast<LPCWSTR>(heystack) + (lenHeystack - 1)) == needle;
688 bool CStringUtils::EndsWithI(const CString& heystack, const wchar_t* needle)
690 const auto lenNeedle = wcslen(needle);
691 const auto lenHeystack = static_cast<size_t>(heystack.GetLength());
692 if (lenNeedle > lenHeystack)
693 return false;
694 return _wcsnicmp(static_cast<LPCWSTR>(heystack) + (lenHeystack - lenNeedle), needle, lenNeedle) == 0;
697 bool CStringUtils::StartsWithI(const wchar_t* heystack, const CString& needle)
699 return _wcsnicmp(heystack, needle, needle.GetLength()) == 0;
702 bool CStringUtils::WriteStringToTextFile(LPCWSTR path, LPCWSTR text, bool bUTF8 /* = true */)
704 return WriteStringToTextFile(static_cast<const std::wstring&>(path), static_cast<const std::wstring&>(text), bUTF8);
707 #endif // #if defined(CSTRING_AVAILABLE) || defined(_MFC_VER)
709 bool CStringUtils::StartsWith(const wchar_t* heystack, const wchar_t* needle)
711 return wcsncmp(heystack, needle, wcslen(needle)) == 0;
714 bool CStringUtils::StartsWith(const char* heystack, const char* needle)
716 return strncmp(heystack, needle, strlen(needle)) == 0;
719 bool CStringUtils::WriteStringToTextFile(const std::wstring& path, const std::wstring& text, bool bUTF8 /* = true */)
721 DWORD dwWritten = 0;
722 CAutoFile hFile = CreateFile(path.c_str(), GENERIC_WRITE, FILE_SHARE_DELETE, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
723 if (!hFile)
724 return false;
726 if (bUTF8)
728 std::string buf = CUnicodeUtils::StdGetUTF8(text);
729 if (!WriteFile(hFile, buf.c_str(), static_cast<DWORD>(buf.length()), &dwWritten, nullptr))
731 return false;
734 else
736 if (!WriteFile(hFile, text.c_str(), static_cast<DWORD>(text.length() * sizeof(wchar_t)), &dwWritten, nullptr))
738 return false;
741 return true;
744 inline static void PipeToNull(wchar_t* ptr)
746 if (*ptr == '|')
747 *ptr = '\0';
750 void CStringUtils::PipesToNulls(wchar_t* buffer, size_t length)
752 wchar_t* ptr = buffer + length;
753 while (ptr != buffer)
755 PipeToNull(ptr);
756 ptr--;
760 void CStringUtils::PipesToNulls(wchar_t* buffer)
762 wchar_t* ptr = buffer;
763 while (*ptr)
765 PipeToNull(ptr);
766 ++ptr;