From fbb7da5d958f2372b9866a89f3f4c3c772d5067c Mon Sep 17 00:00:00 2001 From: Sven Strickroth Date: Thu, 30 Dec 2021 13:15:46 +0100 Subject: [PATCH] Add support for quoted filenames in patches Signed-off-by: Sven Strickroth --- src/TortoiseGitBlame/TortoiseGitBlameData.cpp | 3 +- src/TortoiseGitBlame/TortoiseGitBlameData.h | 4 +-- src/TortoiseMerge/Patch.cpp | 4 +-- src/TortoiseProc/Commands/CleanupCommand.cpp | 20 +---------- src/Utils/StringUtils.cpp | 40 ++++++++++++++++++++++ src/Utils/StringUtils.h | 5 +++ test/UnitTests/PatchTest.cpp | 31 ++++++++++++++++- test/UnitTests/StringUtilsTest.cpp | 15 +++++++- .../resources/patches/quoted-filename.patch | 26 ++++++++++++++ 9 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 test/UnitTests/resources/patches/quoted-filename.patch diff --git a/src/TortoiseGitBlame/TortoiseGitBlameData.cpp b/src/TortoiseGitBlame/TortoiseGitBlameData.cpp index 26f6881b4..6d3dc17e8 100644 --- a/src/TortoiseGitBlame/TortoiseGitBlameData.cpp +++ b/src/TortoiseGitBlame/TortoiseGitBlameData.cpp @@ -480,7 +480,8 @@ GitRevLoglist* CTortoiseGitBlameData::GetRevForHash(CGitHashMap& HashToRev, cons return &(it->second); } -CString CTortoiseGitBlameData::UnquoteFilename(CStringA& s) +// similar code in CStringUtils::UnescapeGitQuotePath +CString CTortoiseGitBlameData::UnquoteFilename(const CStringA& s) { if (s[0] == '"') { diff --git a/src/TortoiseGitBlame/TortoiseGitBlameData.h b/src/TortoiseGitBlame/TortoiseGitBlameData.h index 99b6d08b9..4cf878817 100644 --- a/src/TortoiseGitBlame/TortoiseGitBlameData.h +++ b/src/TortoiseGitBlame/TortoiseGitBlameData.h @@ -1,6 +1,6 @@ // TortoiseGit - a Windows shell extension for easy version control -// Copyright (C) 2008-2013, 2015-2020 - TortoiseGit +// Copyright (C) 2008-2013, 2015-2021 - TortoiseGit // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License @@ -122,7 +122,7 @@ public: private: static GitRevLoglist* GetRevForHash(CGitHashMap& HashToRev, const CGitHash& hash, const CGitMailmap* mailmap, CString* err = nullptr); - static CString UnquoteFilename(CStringA& s); + static CString UnquoteFilename(const CStringA& s); std::vector m_Hash; std::vector m_Dates; diff --git a/src/TortoiseMerge/Patch.cpp b/src/TortoiseMerge/Patch.cpp index 9634b9504..15b89fab3 100644 --- a/src/TortoiseMerge/Patch.cpp +++ b/src/TortoiseMerge/Patch.cpp @@ -139,7 +139,7 @@ BOOL CPatch::ParsePatchFile(CFileTextLines &PatchLines) if (chunks->sFilePath.Find('\t')>=0) chunks->sFilePath = chunks->sFilePath.Left(chunks->sFilePath.Find('\t')); if (CStringUtils::StartsWith(chunks->sFilePath, L"\"") && CStringUtils::EndsWith(chunks->sFilePath, L'"')) - chunks->sFilePath=chunks->sFilePath.Mid(1, chunks->sFilePath.GetLength() - 2); + chunks->sFilePath = CStringUtils::UnescapeGitQuotePath(chunks->sFilePath.Mid(1, chunks->sFilePath.GetLength() - 1)); if (CStringUtils::StartsWith(chunks->sFilePath, L"a/")) chunks->sFilePath=chunks->sFilePath.Mid(static_cast(wcslen(L"a/"))); @@ -195,7 +195,7 @@ BOOL CPatch::ParsePatchFile(CFileTextLines &PatchLines) if (chunks->sFilePath2.Find('\t')>=0) chunks->sFilePath2 = chunks->sFilePath2.Left(chunks->sFilePath2.Find('\t')); if (CStringUtils::StartsWith(chunks->sFilePath2, L"\"") && chunks->sFilePath2.ReverseFind(L'"') == chunks->sFilePath2.GetLength() - 1) - chunks->sFilePath2=chunks->sFilePath2.Mid(1, chunks->sFilePath2.GetLength() - 2); + chunks->sFilePath2 = CStringUtils::UnescapeGitQuotePath(chunks->sFilePath2.Mid(1, chunks->sFilePath2.GetLength() - 1)); if (CStringUtils::StartsWith(chunks->sFilePath2, L"a/")) chunks->sFilePath2=chunks->sFilePath2.Mid(static_cast(wcslen(L"a/"))); diff --git a/src/TortoiseProc/Commands/CleanupCommand.cpp b/src/TortoiseProc/Commands/CleanupCommand.cpp index 417016940..1e22bda43 100644 --- a/src/TortoiseProc/Commands/CleanupCommand.cpp +++ b/src/TortoiseProc/Commands/CleanupCommand.cpp @@ -27,24 +27,6 @@ #include "../Utils/UnicodeUtils.h" #include "SysProgressDlg.h" -static CString UnescapeQuotePath(CString s) -{ - CStringA t; - for (int i = 0; i < s.GetLength(); ++i) - { - if (s[i] == '\\' && i + 3 < s.GetLength()) - { - char c = static_cast((s[i + 1] - '0') * 64 + (s[i + 2] - '0') * 8 + (s[i + 3] - '0')); - t += c; - i += 3; - } - else - t += s[i]; - } - - return CUnicodeUtils::GetUnicode(t); -} - struct SubmodulePayload { STRING_VECTOR &list; @@ -142,7 +124,7 @@ static bool GetFilesToCleanUp(CTGitPathList& delList, const CString& baseCmd, CG { CString tempPath = token.Mid(static_cast(wcslen(L"Would remove "))).TrimRight(); if (quotepath) - tempPath = UnescapeQuotePath(tempPath.Trim(L'"')); + tempPath = CStringUtils::UnescapeGitQuotePath(tempPath.Trim(L'"')); delList.AddPath(pGit->CombinePath(tempPath)); } diff --git a/src/Utils/StringUtils.cpp b/src/Utils/StringUtils.cpp index 891faf15a..9e20a1d07 100644 --- a/src/Utils/StringUtils.cpp +++ b/src/Utils/StringUtils.cpp @@ -491,6 +491,46 @@ static void cleanup_space(CString& string) } } +// similar code in CTortoiseGitBlameData::UnquoteFilename +CString CStringUtils::UnescapeGitQuotePath(const CString& s) +{ + CStringA t; + int i_size = s.GetLength(); + bool isEscaped = false; + for (int i = 0; i < i_size; ++i) + { + wchar_t c = s[i]; + if (isEscaped) + { + if (c >= '0' && c <= '3') + { + if (i + 2 < i_size) + { + c = (((c - L'0') & 03) << 6) | (((s[i + 1] - L'0') & 07) << 3) | ((s[i + 2] - L'0') & 07); + i += 2; + t += c; + } + } + else + { + // we're on purpose not supporting all possible abbreviations such as \n, \r, \t here as these filenames are invalid on Windows anaway + t += c; + } + isEscaped = false; + } + else + { + if (c == L'\\') + isEscaped = true; + else if (c == L'"') + break; + else + t += c; + } + } + return CUnicodeUtils::GetUnicode(t); +} + static void get_sane_name(CString* out, const CString* name, const CString& email) { const CString* src = name; diff --git a/src/Utils/StringUtils.h b/src/Utils/StringUtils.h index e7565c917..da273f9b8 100644 --- a/src/Utils/StringUtils.h +++ b/src/Utils/StringUtils.h @@ -187,6 +187,11 @@ public: static CString WordWrap(const CString& longstring, int limit, bool bCompactPaths, bool bForceWrap, int tabSize); static std::vector WordWrap(const CString& longstring, int limit, int tabSize); /** + * Unescapes Git quoted filenames + * This is not a full implementation of the unescaper as we skip some conversions that will result in invalid filenames. + */ + static CString UnescapeGitQuotePath(const CString& s); + /** * Find and return the number n of starting characters equal between * \ref lhs and \ref rhs. (max n: lhs.Left(n) == rhs.Left(n)) */ diff --git a/test/UnitTests/PatchTest.cpp b/test/UnitTests/PatchTest.cpp index f2134cac8..4424d89e3 100644 --- a/test/UnitTests/PatchTest.cpp +++ b/test/UnitTests/PatchTest.cpp @@ -1,6 +1,6 @@ // TortoiseGit - a Windows shell extension for easy version control -// Copyright (C) 2018-2019 - TortoiseGit +// Copyright (C) 2018-2019, 2021 - TortoiseGit // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License @@ -285,3 +285,32 @@ TEST(CPatch, Parse_GitDiffPatch_SingleLineChange) ASSERT_EQ(static_cast(PATCHSTATE_ADDED), chunk->arLinesStates.GetAt(1)); } } + +TEST(CPatch, Parse_QuotedFilename) +{ + CString resourceDir; + ASSERT_TRUE(GetResourcesDir(resourceDir)); + + CPatch patch; + EXPECT_TRUE(patch.OpenUnifiedDiffFile(resourceDir + L"\\patches\\quoted-filename.patch")); + EXPECT_STREQ(L"", patch.GetErrorMessage()); + EXPECT_EQ(1, patch.GetNumberOfFiles()); + + EXPECT_STREQ(L"ödp.txt", patch.GetFilename(0)); + EXPECT_STREQ(L"ödp.txt", patch.GetFilename2(0)); + EXPECT_STREQ(L"279294a", patch.GetRevision(0)); + EXPECT_STREQ(L"1c7369e", patch.GetRevision2(0)); + + // internals + { + // changed file + auto& chunks = patch.GetChunks(0); + ASSERT_EQ(size_t(1), chunks.size()); + auto chunk = chunks[0].get(); + EXPECT_EQ(8, chunk->arLines.GetCount()); + EXPECT_EQ(6, chunk->lAddLength); + EXPECT_EQ(6, chunk->lRemoveLength); + EXPECT_EQ(1, chunk->lAddStart); + EXPECT_EQ(1, chunk->lRemoveStart); + } +} diff --git a/test/UnitTests/StringUtilsTest.cpp b/test/UnitTests/StringUtilsTest.cpp index 3b11fd06f..835f33566 100644 --- a/test/UnitTests/StringUtilsTest.cpp +++ b/test/UnitTests/StringUtilsTest.cpp @@ -1,6 +1,6 @@ // TortoiseGit - a Windows shell extension for easy version control -// Copyright (C) 2015-2017 - TortoiseGit +// Copyright (C) 2015-2017, 2021 - TortoiseGit // Copyright (C) 2003-2011 - TortoiseSVN // This program is free software; you can redistribute it and/or @@ -336,3 +336,16 @@ TEST(CStringUtils, EndsWithI) EXPECT_FALSE(CStringUtils::EndsWithI(heystack, L"text")); EXPECT_FALSE(CStringUtils::EndsWithI(heystack, L"xt")); } + +TEST(CStringUtils, UnescapeGitQuotePath) +{ + EXPECT_STREQ(L"", CStringUtils::UnescapeGitQuotePath(L"")); + + EXPECT_STREQ(L"ascii.txt", CStringUtils::UnescapeGitQuotePath(L"ascii.txt")); + EXPECT_STREQ(L"ümlauts.txt", CStringUtils::UnescapeGitQuotePath(L"\\303\\274mlauts.txt")); + EXPECT_STREQ(L"umläütß", CStringUtils::UnescapeGitQuotePath(L"uml\\303\\244\\303\\274t\\303\\237")); + + // taken from Git tests: + EXPECT_STREQ(L"\u6FF1\u91CE/file", CStringUtils::UnescapeGitQuotePath(L"\\346\\277\\261\\351\\207\\216/file")); + EXPECT_STREQ(L"\u6FF1\u91CE\u7D14", CStringUtils::UnescapeGitQuotePath(L"\\346\\277\\261\\351\\207\\216\\347\\264\\224")); +} diff --git a/test/UnitTests/resources/patches/quoted-filename.patch b/test/UnitTests/resources/patches/quoted-filename.patch new file mode 100644 index 000000000..233b52053 --- /dev/null +++ b/test/UnitTests/resources/patches/quoted-filename.patch @@ -0,0 +1,26 @@ +From a6e39d29230cfe975a0095bec47f55a7484a0960 Mon Sep 17 00:00:00 2001 +From: Sven Strickroth +Date: Thu, 30 Dec 2021 01:18:01 +0100 +Subject: [PATCH] step 2 + +--- + "\303\266dp.txt" | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git "a/\303\266dp.txt" "b/\303\266dp.txt" +index 279294a..1c7369e 100644 +--- "a/\303\266dp.txt" ++++ "b/\303\266dp.txt" +@@ -1,6 +1,6 @@ + dsfds +-ä ++ääößjfjdf + dsf +-ädsf ++fddsfk38rä##ädsf + dsf + sdf +\ No newline at end of file +-- +2.34.0.windows.1 + -- 2.11.4.GIT