From 7119c58af31bf08d18cad5ea6dbf944d75165d8d Mon Sep 17 00:00:00 2001 From: Nir Bar Date: Wed, 13 Nov 2024 15:33:29 +0200 Subject: [PATCH] RemoveFolderEx: Remove a folder recursively. Differs from WixUtilExtension's RemoveFolderEx in proper handling of reparse points (symbolic links, mount volumes, etc.) Reparse points are deleted before RemoveFiles action. On rollback, the reparse points are restored Worksaround Windows Installer bugous handling of reparse points- such as deleting the target of symbolic link files, or keeping directory symbolic link if their target contains files --- src/CaCommon/WixString.h | 26 +- src/PanelSwCustomActions/CommonDeferred.cpp | 6 + src/PanelSwCustomActions/FileOperations.cpp | 381 ++++++++++---- src/PanelSwCustomActions/FileOperations.h | 25 +- .../PanelSwCustomActions.def | 2 + .../PanelSwCustomActions.vcxproj | 4 +- .../PanelSwCustomActions.vcxproj.filters | 5 +- src/PanelSwCustomActions/ReparsePoint.cpp | 463 ++++++++++++++++++ src/PanelSwCustomActions/ReparsePoint.h | 30 ++ .../PanelSwWiBackendBinder.cs | 1 + src/PanelSwWixExtension/PanelSwWixCompiler.cs | 46 +- .../Symbols/PSW_RemoveFolderEx.cs | 62 +++ .../Xsd/PanelSwWixExtension.xsd | 32 ++ src/ProtoCaLib/ProtoCaLib.vcxproj | 11 +- src/ProtoCaLib/ProtoCaLib.vcxproj.filters | 10 +- src/ProtoCaLib/reparsePointDetails.proto | 14 + src/README.md | 1 + src/TidyBuild.custom.props | 2 +- .../FileOperationsUT/FileOperationsUT.wixproj | 3 + .../FileOperationsUT/FileOperationsUT.wxs | 20 + src/UnitTests/FileOperationsUT/run-test.bat | 188 +++++++ src/wixlib/PanelSwWixExtension.wxs | 31 ++ src/wixlib/en-US.wxl | 2 + src/wixlib/he-IL.wxl | 2 + 24 files changed, 1245 insertions(+), 122 deletions(-) create mode 100644 src/PanelSwCustomActions/ReparsePoint.cpp create mode 100644 src/PanelSwCustomActions/ReparsePoint.h create mode 100644 src/PanelSwWixExtension/Symbols/PSW_RemoveFolderEx.cs create mode 100644 src/ProtoCaLib/reparsePointDetails.proto create mode 100644 src/UnitTests/FileOperationsUT/run-test.bat diff --git a/src/CaCommon/WixString.h b/src/CaCommon/WixString.h index e7d41a5a..e339f796 100644 --- a/src/CaCommon/WixString.h +++ b/src/CaCommon/WixString.h @@ -134,7 +134,7 @@ class CWixString return pS; } - HRESULT Copy(const CWixString &other) + HRESULT Copy(const CWixString& other) { HRESULT hr = S_OK; @@ -208,7 +208,7 @@ class CWixString ExitOnNull((dwStart < StrLen()), hr, E_INVALIDSTATE, "Substring start index is out of range"); szOld = Detach(); - + hr = Copy(szOld + dwStart, dwLength); ExitOnFailure(hr, "Failed to copy string"); @@ -259,7 +259,7 @@ class CWixString LPWSTR szObfuscated = nullptr; LPWSTR szMsiHiddenProperties = nullptr; LPWSTR szHideMe = nullptr; - + Release(); hr = StrAllocString(&szStripped, szFormat, 0); @@ -384,6 +384,26 @@ class CWixString return hr; } + // Remove characters from the left + void RemoveLeft(DWORD dwCount) + { + DWORD dwNewLen = 0; + DWORD i = 0; + + if (dwCount >= StrLen()) + { + Release(); + return; + } + + dwNewLen = StrLen() - dwCount; + for (i = 0; i < dwNewLen; ++i) + { + _pS[i] = _pS[i + dwCount]; + } + _pS[i] = NULL; + } + #pragma region Tokenize HRESULT Tokenize(LPCWSTR delimiters, LPCWSTR* firstToken) diff --git a/src/PanelSwCustomActions/CommonDeferred.cpp b/src/PanelSwCustomActions/CommonDeferred.cpp index e46a442b..b8e77035 100644 --- a/src/PanelSwCustomActions/CommonDeferred.cpp +++ b/src/PanelSwCustomActions/CommonDeferred.cpp @@ -13,6 +13,7 @@ #include "XslTransform.h" #include "RestartLocalResources.h" #include "ConcatFiles.h" +#include "ReparsePoint.h" // ReceiverToExecutorFunc implementation. HRESULT ReceiverToExecutor(LPCSTR szReceiver, CDeferredActionBase** ppExecutor) @@ -90,6 +91,11 @@ HRESULT ReceiverToExecutor(LPCSTR szReceiver, CDeferredActionBase** ppExecutor) WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Creating ConcatFiles handler"); (*ppExecutor) = new CConcatFiles(); } + else if (0 == ::strcmp(szReceiver, "CReparsePoint")) + { + WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Creating ReparsePoint handler"); + (*ppExecutor) = new CReparsePoint(); + } else { hr = E_INVALIDARG; diff --git a/src/PanelSwCustomActions/FileOperations.cpp b/src/PanelSwCustomActions/FileOperations.cpp index 638bc35b..9cbaf047 100644 --- a/src/PanelSwCustomActions/FileOperations.cpp +++ b/src/PanelSwCustomActions/FileOperations.cpp @@ -1,11 +1,140 @@ #include "pch.h" #include "FileOperations.h" +#include "ReparsePoint.h" #include #include #include "fileOperationsDetails.pb.h" using namespace ::com::panelsw::ca; using namespace google::protobuf; +extern "C" UINT __stdcall RemoveFolderEx(MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + PMSIHANDLE hView; + PMSIHANDLE hRecord; + MSIHANDLE hRemoveFileTable = NULL; + MSIHANDLE hRemoveFileColumns = NULL; + DWORD dwRes = 0; + CFileOperations::FILE_ENTRY rootFileEntry; + LPWSTR* pszFolders = nullptr; + UINT cFolders = 0; + + hr = WcaInitialize(hInstall, __FUNCTION__); + ExitOnFailure(hr, "Failed to initialize"); + WcaLog(LOGMSG_STANDARD, "Initialized from PanelSwCustomActions " FullVersion); + + // Ensure table PSW_RemoveFolderEx exists. + hr = WcaTableExists(L"PSW_RemoveFolderEx"); + ExitOnFailure(hr, "Failed to check if table exists 'PSW_RemoveFolderEx'"); + ExitOnNull((hr == S_OK), hr, E_FAIL, "Table does not exist 'PSW_RemoveFolderEx'. Have you authored 'PanelSw:RemoveFolderEx' entries in WiX code?"); + + // Execute view + hr = WcaOpenExecuteView(L"SELECT `Component_`, `Property`, `InstallMode` FROM `PSW_RemoveFolderEx`", &hView); + ExitOnFailure(hr, "Failed to execute SQL query"); + + // Iterate records + while ((hr = WcaFetchRecord(hView, &hRecord)) != E_NOMOREITEMS) + { + ExitOnFailure(hr, "Failed to fetch record."); + CFileOperations::ReleaseFileEntries(&rootFileEntry); + ReleaseNullStrArray(pszFolders, cFolders); + + // Get fields + CWixString szComponent, szBaseProperty; + int flags = 0; + + hr = WcaGetRecordString(hRecord, 1, (LPWSTR*)szComponent); + ExitOnFailure(hr, "Failed to get Component_."); + hr = WcaGetRecordString(hRecord, 2, (LPWSTR*)szBaseProperty); + ExitOnFailure(hr, "Failed to get Property."); + hr = WcaGetRecordInteger(hRecord, 3, &flags); + ExitOnFailure(hr, "Failed to get Flags."); + + hr = WcaGetProperty(szBaseProperty, &rootFileEntry.szPath); + ExitOnFailure(hr, "Failed to get property"); + + if (!rootFileEntry.szPath || !*rootFileEntry.szPath) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveFolderEx for property '%ls' because it is empty", (LPCWSTR)szBaseProperty); + continue; + } + + rootFileEntry.dwAttributes = ::GetFileAttributes(rootFileEntry.szPath); + if (rootFileEntry.dwAttributes == INVALID_FILE_ATTRIBUTES) + { + dwRes = ::GetLastError(); + ExitOnNullWithLastError(((dwRes == ERROR_FILE_NOT_FOUND) || (dwRes == ERROR_PATH_NOT_FOUND)), hr, "Failed to get file attributes for path '%ls'", rootFileEntry.szPath); + + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveFolderEx for property '%ls' because path '%ls' does not exist", (LPCWSTR)szBaseProperty, rootFileEntry.szPath); + continue; + } + + if ((rootFileEntry.dwAttributes & FILE_ATTRIBUTE_DIRECTORY) != FILE_ATTRIBUTE_DIRECTORY) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveFolderEx for property '%ls' because '%ls' is not a folder", (LPCWSTR)szBaseProperty, rootFileEntry.szPath); + continue; + } + + hr = CFileOperations::ListFileEntries(&rootFileEntry, L"*", true); + ExitOnFailure(hr, "Failed to list file entries under '%ls'", rootFileEntry.szPath); + + hr = WcaAddTempRecord(&hRemoveFileTable, &hRemoveFileColumns, L"RemoveFile", nullptr, 1, 5, L"RfxFolder", (LPCWSTR)szComponent, nullptr, (LPCWSTR)szBaseProperty, flags); + ExitOnFailure(hr, "Failed to add temporary row table"); + + if (CReparsePoint::IsSymbolicLinkOrMount(rootFileEntry.szPath)) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveFolderEx for files under '%ls' because it is a symbolic link or mount folder", rootFileEntry.szPath); + continue; + } + + hr = WcaAddTempRecord(&hRemoveFileTable, &hRemoveFileColumns, L"RemoveFile", nullptr, 1, 5, L"RfxFiles", (LPCWSTR)szComponent, L"*", (LPCWSTR)szBaseProperty, flags); + ExitOnFailure(hr, "Failed to add temporary row table"); + + hr = CFileOperations::FilterFileEntries(&rootFileEntry, FILE_ATTRIBUTE_DIRECTORY, 0, &pszFolders, &cFolders); + ExitOnFailure(hr, "Failed to filters sub folders of '%ls'", rootFileEntry.szPath); + + for (UINT i = 1/*Handled root above*/; i < cFolders; ++i) + { + CWixString szDirProperty; + + hr = szDirProperty.Format(L"_DIR_%ls_%u", (LPCWSTR)szBaseProperty, i); + ExitOnFailure(hr, "Failed to format string"); + + hr = WcaSetProperty((LPCWSTR)szDirProperty, pszFolders[i]); + ExitOnFailure(hr, "Failed to set property"); + + hr = WcaAddTempRecord(&hRemoveFileTable, &hRemoveFileColumns, L"RemoveFile", nullptr, 1, 5, L"RfxFolder", (LPCWSTR)szComponent, nullptr, (LPCWSTR)szDirProperty, flags); + ExitOnFailure(hr, "Failed to add temporary row table"); + + if (CReparsePoint::IsSymbolicLinkOrMount(pszFolders[i])) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveFolderEx for files under '%ls' because it is a symbolic link or mount folder", pszFolders[i]); + continue; + } + + hr = WcaAddTempRecord(&hRemoveFileTable, &hRemoveFileColumns, L"RemoveFile", nullptr, 1, 5, L"RfxFiles", (LPCWSTR)szComponent, L"*", (LPCWSTR)szDirProperty, flags); + ExitOnFailure(hr, "Failed to add temporary row table"); + } + } + hr = S_OK; + +LExit: + CFileOperations::ReleaseFileEntries(&rootFileEntry); + ReleaseStrArray(pszFolders, cFolders); + if (hRemoveFileTable) + { + ::MsiCloseHandle(hRemoveFileTable); + } + if (hRemoveFileColumns) + { + ::MsiCloseHandle(hRemoveFileColumns); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + extern "C" UINT __stdcall DeletePath(MSIHANDLE hInstall) { HRESULT hr = S_OK; @@ -301,14 +430,21 @@ HRESULT CFileOperations::DeletePath(LPCWSTR szFrom, bool bIgnoreMissing, bool bI SHFILEOPSTRUCT opInfo; HRESULT hr = S_OK; INT nRes = ERROR_SUCCESS; + CFileOperations::FILE_ENTRY rootFileEntry; LPWSTR szFromNull = nullptr; LPWSTR* pszFiles = nullptr; UINT nFiles = 0; if (bOnlyIfEmpty && ::PathIsDirectory(szFrom)) { - hr = ListFiles(szFrom, L"*", true, &pszFiles, &nFiles); - ExitOnFailure(hr, "Failed testing wether folder '%ls' is empty", szFrom); + hr = StrAllocString(&rootFileEntry.szPath, szFrom, 0); + ExitOnFailure(hr, "Failed to copy string"); + + hr = ListFileEntries(&rootFileEntry, L"*", true); + ExitOnFailure(hr, "Failed to enumerate file entries under '%ls'", szFrom); + + hr = FilterFileEntries(&rootFileEntry, INVALID_FILE_ATTRIBUTES, FILE_ATTRIBUTE_DIRECTORY, &pszFiles, &nFiles); + ExitOnFailure(hr, "Failed to filter file entries under '%ls'", szFrom); if (nFiles > 0) { @@ -347,18 +483,21 @@ HRESULT CFileOperations::DeletePath(LPCWSTR szFrom, bool bIgnoreMissing, bool bI // MoveFileEx can delete empty folder only, so we must explictly delete files first if (::PathIsDirectory(szFrom)) { - if (pszFiles) + ReleaseNullStrArray(pszFiles, nFiles); + + if (!rootFileEntry.szPath || !*rootFileEntry.szPath) { - ReleaseNullStrArray(pszFiles, nFiles); - } + hr = StrAllocString(&rootFileEntry.szPath, szFrom, 0); + ExitOnFailure(hr, "Failed to copy string"); - hr = ListFiles(szFrom, L"*", true, &pszFiles, &nFiles); - ExitOnFailure(hr, "Failed listing files in folder '%ls'", szFrom); + hr = ListFileEntries(&rootFileEntry, L"*", true); + ExitOnFailure(hr, "Failed to enumerate file entries under '%ls'", szFrom); + } - hr = ListSubFolders(szFrom, &pszFiles, &nFiles); - ExitOnFailure(hr, "Failed listing subfolders of '%ls'", szFrom); + hr = FilterFileEntries(&rootFileEntry, INVALID_FILE_ATTRIBUTES, 0, &pszFiles, &nFiles); + ExitOnFailure(hr, "Failed filtering files entries under '%ls'", szFrom); - for (UINT i = 0; i < nFiles; ++i) + for (UINT i = 1 /*Skip self*/; i < nFiles; ++i) { DeletePath(pszFiles[i], bIgnoreMissing, bIgnoreErrors, bOnlyIfEmpty, bAllowReboot); } @@ -378,6 +517,7 @@ HRESULT CFileOperations::DeletePath(LPCWSTR szFrom, bool bIgnoreMissing, bool bI ExitOnNull((!opInfo.fAnyOperationsAborted), hr, E_FAIL, "Failed deleting file (operation aborted)"); LExit: + ReleaseFileEntries(&rootFileEntry); ReleaseStrArray(pszFiles, nFiles); ReleaseStr(szFromNull); @@ -520,94 +660,122 @@ HRESULT CFileOperations::PathToDevicePath(LPCWSTR szPath, LPWSTR* pszDevicePath) } // static -HRESULT CFileOperations::ListSubFolders(LPCWSTR szBaseFolder, LPWSTR** pszFolders, UINT* pcFolder) +HRESULT CFileOperations::ListSubFolders(LPCWSTR szBaseFolder, LPWSTR** ppszFolders, UINT* pcFolder) { HRESULT hr = S_OK; - LPWSTR szFullPattern = nullptr; - LPWSTR szFullFolder = nullptr; - LPWSTR szCurrFile = nullptr; - WIN32_FIND_DATA FindFileData; - HANDLE hFind = INVALID_HANDLE_VALUE; + CFileOperations::FILE_ENTRY rootFileEntry; - if (IsSymbolicLinkOrMount(szBaseFolder)) - { - WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Folder '%ls' is a symbolic link or a mount point, so not enumerating its files", szBaseFolder); - ExitFunction(); - } - if (!DirExists(szBaseFolder, nullptr)) - { - WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Folder '%ls' doesn't exist", szBaseFolder); - ExitFunction(); - } + hr = StrAllocString(&rootFileEntry.szPath, szBaseFolder, 0); + ExitOnFailure(hr, "Failed to copy string"); - hr = StrAllocString(&szFullFolder, szBaseFolder, 0); - ExitOnFailure(hr, "Failed allocating string"); + ::PathRemoveBackslash(rootFileEntry.szPath); - hr = PathBackslashTerminate(&szFullFolder); - ExitOnFailure(hr, "Failed allocating string"); + hr = ListFileEntries(&rootFileEntry, L"*", true); + ExitOnFailure(hr, "Failed to enumerate file entries under '%ls'", szBaseFolder); - hr = StrAllocFormatted(&szFullPattern, L"%ls*", szFullFolder); - ExitOnFailure(hr, "Failed allocating string"); + hr = FilterFileEntries(&rootFileEntry, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, ppszFolders, pcFolder); + ExitOnFailure(hr, "Failed to enumerate folders under '%ls'", szBaseFolder); - hFind = ::FindFirstFile(szFullPattern, &FindFileData); - ExitOnNullWithLastError((hFind != INVALID_HANDLE_VALUE), hr, "Failed searching files in '%ls'", szFullFolder); +LExit: + ReleaseFileEntries(&rootFileEntry); - do - { - if ((::wcscmp(L".", FindFileData.cFileName) == 0) || (::wcscmp(L"..", FindFileData.cFileName) == 0)) - { - continue; - } + return hr; +} - if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) - { - ReleaseNullStr(szCurrFile); +// static +HRESULT CFileOperations::ListFiles(LPCWSTR szFolder, LPCWSTR szPattern, bool bRecursive, LPWSTR** ppszFiles, UINT* pcFiles) +{ + HRESULT hr = S_OK; + CFileOperations::FILE_ENTRY rootFileEntry; - hr = StrAllocFormatted(&szCurrFile, L"%ls%ls", szFullFolder, FindFileData.cFileName); - ExitOnFailure(hr, "Failed allocating string"); + hr = StrAllocString(&rootFileEntry.szPath, szFolder, 0); + ExitOnFailure(hr, "Failed to copy string"); - hr = StrArrayAllocString(pszFolders, pcFolder, szCurrFile, 0); - ExitOnFailure(hr, "Failed allocating string"); + ::PathRemoveBackslash(rootFileEntry.szPath); - hr = ListSubFolders(szCurrFile, pszFolders, pcFolder); - ExitOnFailure(hr, "Failed finding files"); - } + hr = ListFileEntries(&rootFileEntry, szPattern, bRecursive); + ExitOnFailure(hr, "Failed to enumerate file entries under '%ls'", szFolder); + + hr = FilterFileEntries(&rootFileEntry, ~FILE_ATTRIBUTE_DIRECTORY, 0, ppszFiles, pcFiles); + ExitOnFailure(hr, "Failed to enumerate folders under '%ls'", szFolder); + +LExit: + ReleaseFileEntries(&rootFileEntry); + + return hr; +} + +/*static*/ HRESULT CFileOperations::ListReparsePoints(LPCWSTR szFolder, LPWSTR** ppszReparsePoints, UINT* pcReparsePoints) +{ + HRESULT hr = S_OK; + CFileOperations::FILE_ENTRY rootFileEntry; + + hr = StrAllocString(&rootFileEntry.szPath, szFolder, 0); + ExitOnFailure(hr, "Failed to copy string"); + + ::PathRemoveBackslash(rootFileEntry.szPath); - } while (::FindNextFile(hFind, &FindFileData)); - ExitOnNullWithLastError((::GetLastError() == ERROR_NO_MORE_FILES), hr, "Failed searching files in '%ls'", szFullFolder); + hr = ListFileEntries(&rootFileEntry, L"*", true); + ExitOnFailure(hr, "Failed to enumerate file entries under '%ls'", szFolder); - ::FindClose(hFind); - hFind = INVALID_HANDLE_VALUE; + hr = FilterFileEntries(&rootFileEntry, FILE_ATTRIBUTE_REPARSE_POINT, 0, ppszReparsePoints, pcReparsePoints); + ExitOnFailure(hr, "Failed to enumerate folders under '%ls'", szFolder); LExit: + ReleaseFileEntries(&rootFileEntry); - ReleaseStr(szFullPattern); - ReleaseStr(szCurrFile); - if (hFind && (hFind != INVALID_HANDLE_VALUE)) + return hr; +} + +/*static*/ HRESULT CFileOperations::FilterFileEntries(CFileOperations::FILE_ENTRY* pRootEntry, DWORD dwAttributesInclude, DWORD dwAttributesExclude, LPWSTR** pszFiltered, UINT* pcFiltered) +{ + HRESULT hr = S_OK; + + // Callers expect to get the root entry first! + if ((pRootEntry->dwAttributes & dwAttributesInclude) && !(pRootEntry->dwAttributes & dwAttributesExclude)) { - ::FindClose(hFind); + hr = StrArrayAllocString(pszFiltered, pcFiltered, pRootEntry->szPath, 0); + ExitOnFailure(hr, "Failed to add entry '%ls' to string array", pRootEntry->szPath); } + for (UINT i = 0; i < pRootEntry->cSubEntries; ++i) + { + hr = FilterFileEntries(&pRootEntry->pSubEntries[i], dwAttributesInclude, dwAttributesExclude, pszFiltered, pcFiltered); + ExitOnFailure(hr, "Failed to add subentry of '%ls' to string array", pRootEntry->szPath); + } + +LExit: return hr; } -// static -HRESULT CFileOperations::ListFiles(LPCWSTR szFolder, LPCWSTR szPattern, bool bRecursive, LPWSTR** pszFiles, UINT* pcFiles) +/*static*/ HRESULT CFileOperations::ListFileEntries(CFileOperations::FILE_ENTRY* pRootFolder, LPCWSTR szPattern, bool bRecursive) { HRESULT hr = S_OK; LPWSTR szFullPattern = nullptr; LPWSTR szFullFolder = nullptr; - LPWSTR szCurrFile = nullptr; WIN32_FIND_DATA FindFileData; HANDLE hFind = INVALID_HANDLE_VALUE; - if (IsSymbolicLinkOrMount(szFolder)) + ExitOnNull((pRootFolder->szPath && *pRootFolder->szPath), hr, E_INVALIDARG, "Path is NULL or empty"); + + if (pRootFolder->dwAttributes == INVALID_FILE_ATTRIBUTES) { - WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Folder '%ls' is a symbolic link or a mount point, so not enumerating its files", szFolder); + pRootFolder->dwAttributes = ::GetFileAttributes(pRootFolder->szPath); + ExitOnNullWithLastError((pRootFolder->dwAttributes != INVALID_FILE_ATTRIBUTES), hr, "Failed getting file attributes for '%ls'", pRootFolder->szPath); + } + if ((pRootFolder->dwAttributes & FILE_ATTRIBUTE_DIRECTORY) != FILE_ATTRIBUTE_DIRECTORY) + { + hr = S_FALSE; + ExitFunction(); + } + if (CReparsePoint::IsSymbolicLinkOrMount(pRootFolder->szPath)) + { + WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Folder '%ls' is a symbolic link or a mount point, so not enumerating its files", pRootFolder->szPath); + hr = S_FALSE; ExitFunction(); } - hr = StrAllocString(&szFullFolder, szFolder, 0); + hr = StrAllocString(&szFullFolder, pRootFolder->szPath, 0); ExitOnFailure(hr, "Failed allocating string"); hr = PathBackslashTerminate(&szFullFolder); @@ -625,7 +793,7 @@ HRESULT CFileOperations::ListFiles(LPCWSTR szFolder, LPCWSTR szPattern, bool bRe DWORD dwErr = ::GetLastError(); ExitOnNullWithLastError(((dwErr == ERROR_FILE_NOT_FOUND) || (dwErr == ERROR_PATH_NOT_FOUND)), hr, "Failed searching files in '%ls'", szFullFolder); } - else + else { do { @@ -634,22 +802,30 @@ HRESULT CFileOperations::ListFiles(LPCWSTR szFolder, LPCWSTR szPattern, bool bRe continue; } - if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + if (((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) && !CReparsePoint::IsSymbolicLinkOrMount(&FindFileData)) { - ReleaseNullStr(szCurrFile); + CFileOperations::FILE_ENTRY* pNewEntry = nullptr; + + hr = MemReAllocArray((void**)&pRootFolder->pSubEntries, pRootFolder->cSubEntries, sizeof(CFileOperations::FILE_ENTRY), 1); + ExitOnFailure(hr, "Failed reallocating array"); + + ++pRootFolder->cSubEntries; + pNewEntry = &pRootFolder->pSubEntries[pRootFolder->cSubEntries - 1]; + pNewEntry->dwAttributes = FindFileData.dwFileAttributes; + pNewEntry->cSubEntries = 0; + pNewEntry->pSubEntries = nullptr; - hr = StrAllocFormatted(&szCurrFile, L"%ls%ls", szFullFolder, FindFileData.cFileName); + hr = StrAllocFormatted(&pNewEntry->szPath, L"%ls%ls", szFullFolder, FindFileData.cFileName); ExitOnFailure(hr, "Failed allocating string"); - hr = ListFiles(szCurrFile, szPattern, bRecursive, pszFiles, pcFiles); - ExitOnFailure(hr, "Failed finding files"); + hr = ListFileEntries(pNewEntry, szPattern, bRecursive); + ExitOnFailure(hr, "Failed finding file entries under '%ls'", pNewEntry->szPath); } } while (::FindNextFile(hFind, &FindFileData)); ExitOnNullWithLastError((::GetLastError() == ERROR_NO_MORE_FILES), hr, "Failed searching files in '%ls'", szFullFolder); - ::FindClose(hFind); - hFind = INVALID_HANDLE_VALUE; + ReleaseFileFindHandle(hFind); } } @@ -672,21 +848,27 @@ HRESULT CFileOperations::ListFiles(LPCWSTR szFolder, LPCWSTR szPattern, bool bRe DWORD dwErr = ::GetLastError(); ExitOnNullWithLastError(((dwErr == ERROR_FILE_NOT_FOUND) || (dwErr == ERROR_PATH_NOT_FOUND)), hr, "Failed searching files in '%ls'", szFullPattern); } - else + else { do { - if ((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + if (((FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) && !CReparsePoint::IsSymbolicLinkOrMount(&FindFileData)) { continue; } - ReleaseNullStr(szCurrFile); + CFileOperations::FILE_ENTRY* pNewEntry = nullptr; - hr = StrAllocFormatted(&szCurrFile, L"%ls%ls", szFullFolder, FindFileData.cFileName); - ExitOnFailure(hr, "Failed allocating string"); + hr = MemReAllocArray((void**)&pRootFolder->pSubEntries, pRootFolder->cSubEntries, sizeof(CFileOperations::FILE_ENTRY), 1); + ExitOnFailure(hr, "Failed reallocating array"); + + ++pRootFolder->cSubEntries; + pNewEntry = &pRootFolder->pSubEntries[pRootFolder->cSubEntries - 1]; + pNewEntry->dwAttributes = FindFileData.dwFileAttributes; + pNewEntry->cSubEntries = 0; + pNewEntry->pSubEntries = nullptr; - hr = StrArrayAllocString(pszFiles, pcFiles, szCurrFile, 0); + hr = StrAllocFormatted(&pNewEntry->szPath, L"%ls%ls", szFullFolder, FindFileData.cFileName); ExitOnFailure(hr, "Failed allocating string"); } while (::FindNextFile(hFind, &FindFileData)); @@ -694,15 +876,24 @@ HRESULT CFileOperations::ListFiles(LPCWSTR szFolder, LPCWSTR szPattern, bool bRe } LExit: - + ReleaseStr(szFullFolder); ReleaseStr(szFullPattern); - ReleaseStr(szCurrFile); - if (hFind && (hFind != INVALID_HANDLE_VALUE)) + ReleaseFileFindHandle(hFind); + + return hr; +} + +/*static*/ void CFileOperations::ReleaseFileEntries(CFileOperations::FILE_ENTRY* pRootFolder) +{ + for (; pRootFolder->cSubEntries; --pRootFolder->cSubEntries) { - ::FindClose(hFind); + CFileOperations::FILE_ENTRY* pEntry = &pRootFolder->pSubEntries[pRootFolder->cSubEntries - 1]; + ReleaseFileEntries(pEntry); } - return hr; + ReleaseNullMem(pRootFolder->pSubEntries); + ReleaseNullStr(pRootFolder->szPath); + pRootFolder->dwAttributes = INVALID_FILE_ATTRIBUTES; } //static @@ -761,27 +952,3 @@ HRESULT CFileOperations::MakeTemporaryName(LPCWSTR szBackupOf, LPCWSTR szPrefix, LExit: return hr; } - -bool CFileOperations::IsSymbolicLinkOrMount(LPCWSTR szPath) -{ - HANDLE hFind = INVALID_HANDLE_VALUE; - WIN32_FIND_DATA wfaData; - HRESULT hr = S_OK; - - hFind = ::FindFirstFile(szPath, &wfaData); - if (hFind == INVALID_HANDLE_VALUE) - { - hr = S_FALSE; - DWORD dwErr = ::GetLastError(); - ExitOnNullWithLastError(((dwErr == ERROR_FILE_NOT_FOUND) || (dwErr == ERROR_PATH_NOT_FOUND)), hr, "Path '%ls' can't be checked for reparse point tag", szPath); - } - -LExit: - if (hFind && (hFind != INVALID_HANDLE_VALUE)) - { - ::FindClose(hFind); - } - - return (hr == S_OK) ? (((wfaData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT) - && ((wfaData.dwReserved0 == IO_REPARSE_TAG_SYMLINK) || (wfaData.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT))) : false; -} diff --git a/src/PanelSwCustomActions/FileOperations.h b/src/PanelSwCustomActions/FileOperations.h index 57f51be6..6b0cee79 100644 --- a/src/PanelSwCustomActions/FileOperations.h +++ b/src/PanelSwCustomActions/FileOperations.h @@ -14,6 +14,15 @@ class CFileOperations : , AllowReboot = 2 * OnlyIfEmpty }; + typedef struct _FILE_ENTRY + { + LPWSTR szPath = nullptr; + DWORD dwAttributes = INVALID_FILE_ATTRIBUTES; + + _FILE_ENTRY* pSubEntries = nullptr; + UINT cSubEntries = 0; + } FILE_ENTRY, * PFILE_ENTRY; + CFileOperations() : CDeferredActionBase("FileOperations") { } HRESULT AddCopyFile(LPCWSTR szFrom, LPCWSTR szTo, int flags = 0); @@ -23,15 +32,21 @@ class CFileOperations : HRESULT CopyPath(LPCWSTR szFrom, LPCWSTR szTo, bool bMove, bool bIgnoreMissing, bool bIgnoreErrors, bool bOnlyIfEmpty, bool bAllowReboot); HRESULT DeletePath(LPCWSTR szFrom, bool bIgnoreMissing, bool bIgnoreErrors, bool bOnlyIfEmpty, bool bAllowReboot); - static ::com::panelsw::ca::FileRegexDetails::FileEncoding DetectEncoding(const void* pFileContent, DWORD dwSize); static HRESULT PathToDevicePath(LPCWSTR szPath, LPWSTR* pszDevicePath); + + static ::com::panelsw::ca::FileRegexDetails::FileEncoding DetectEncoding(const void* pFileContent, DWORD dwSize); + + static HRESULT MakeTemporaryName(LPCWSTR szBackupOf, LPCWSTR szPrefix, bool bIsFolder, LPWSTR* pszTempName); + + static HRESULT ListFileEntries(CFileOperations::FILE_ENTRY* pRootFolder, LPCWSTR szPattern, bool bRecursive); + static void ReleaseFileEntries(CFileOperations::FILE_ENTRY* pRootFolder); + static HRESULT FilterFileEntries(CFileOperations::FILE_ENTRY* pRootFolder, DWORD dwAttributesInclude, DWORD dwAttributesExclude, LPWSTR** pszFiltered, UINT* pcFiltered); + static HRESULT ListSubFolders(LPCWSTR szBaseFolder, LPWSTR** pszFolders, UINT* pcFolder); static HRESULT ListFiles(LPCWSTR szFolder, LPCWSTR szPattern, bool bRecursive, LPWSTR** pszFiles, UINT* pcFiles); - static HRESULT MakeTemporaryName(LPCWSTR szBackupOf, LPCWSTR szPrefix, bool bIsFolder, LPWSTR* pszTempName); - static bool IsSymbolicLinkOrMount(LPCWSTR szPath); + static HRESULT ListReparsePoints(LPCWSTR szFolder, LPWSTR** pszReparsePoints, UINT* pcReparsePoints); protected: - + HRESULT DeferredExecute(const ::std::string& command) override; }; - diff --git a/src/PanelSwCustomActions/PanelSwCustomActions.def b/src/PanelSwCustomActions/PanelSwCustomActions.def index ec0fa511..4c38b09c 100644 --- a/src/PanelSwCustomActions/PanelSwCustomActions.def +++ b/src/PanelSwCustomActions/PanelSwCustomActions.def @@ -58,3 +58,5 @@ EXPORTS IsWindowsVersionOrGreater ListProcessorFeatures ExecuteCommand + RemoveFolderEx + RemoveReparseDataSched diff --git a/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj b/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj index 521aebcc..338bca01 100644 --- a/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj +++ b/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj @@ -89,6 +89,7 @@ + @@ -128,6 +129,7 @@ + @@ -200,4 +202,4 @@ - + \ No newline at end of file diff --git a/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters b/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters index b4ddba9b..cb4af92e 100644 --- a/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters +++ b/src/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters @@ -263,6 +263,9 @@ Header Files + + Header Files + Header Files @@ -280,7 +283,7 @@ Source Files - + diff --git a/src/PanelSwCustomActions/ReparsePoint.cpp b/src/PanelSwCustomActions/ReparsePoint.cpp new file mode 100644 index 00000000..78aa0109 --- /dev/null +++ b/src/PanelSwCustomActions/ReparsePoint.cpp @@ -0,0 +1,463 @@ +#include "pch.h" +#include "FileOperations.h" +#include "ReparsePoint.h" +#include +#include +#include "reparsePointDetails.pb.h" +using namespace ::com::panelsw::ca; +using namespace google::protobuf; + +// Extract from WDK's ntifs.h +#define FILE_DEVICE_FILE_SYSTEM 0x00000009 +#define METHOD_BUFFERED 0 +#define FILE_ANY_ACCESS 0 +#define FILE_SPECIAL_ACCESS (FILE_ANY_ACCESS) +#define CTL_CODE( DeviceType, Function, Method, Access ) ( \ + ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \ +) +#define FSCTL_SET_REPARSE_POINT CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 41, METHOD_BUFFERED, FILE_SPECIAL_ACCESS) // REPARSE_DATA_BUFFER, +#define FSCTL_GET_REPARSE_POINT CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 42, METHOD_BUFFERED, FILE_ANY_ACCESS) // REPARSE_DATA_BUFFER +#define FSCTL_DELETE_REPARSE_POINT CTL_CODE(FILE_DEVICE_FILE_SYSTEM, 43, METHOD_BUFFERED, FILE_SPECIAL_ACCESS) // REPARSE_DATA_BUFFER, + +typedef struct _REPARSE_DATA_BUFFER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; + + _Field_size_bytes_(ReparseDataLength) + union { + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + ULONG Flags; + WCHAR PathBuffer[1]; + } SymbolicLinkReparseBuffer; + struct { + USHORT SubstituteNameOffset; + USHORT SubstituteNameLength; + USHORT PrintNameOffset; + USHORT PrintNameLength; + WCHAR PathBuffer[1]; + } MountPointReparseBuffer; + struct { + UCHAR DataBuffer[1]; + } GenericReparseBuffer; + } DUMMYUNIONNAME; +} REPARSE_DATA_BUFFER, * PREPARSE_DATA_BUFFER; + +#pragma warning(pop) +#define REPARSE_DATA_BUFFER_HEADER_SIZE UFIELD_OFFSET(REPARSE_DATA_BUFFER, GenericReparseBuffer) +// End extract from WDK's ntifs.h + +typedef struct _REPARSE_DATA_COMMON_HEADER { + ULONG ReparseTag; + USHORT ReparseDataLength; + USHORT Reserved; +} REPARSE_DATA_COMMON_HEADER; + +enum RemoveFileInstallMode +{ + RemoveFileInstallMode_Unknown = 0, + RemoveFileInstallMode_Install = 1, + RemoveFileInstallMode_Uninstall = 2, + RemoveFileInstallMode_Both = 3, +}; + +extern "C" UINT __stdcall RemoveReparseDataSched(MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + UINT er = ERROR_SUCCESS; + DWORD dwRes = 0; + PMSIHANDLE hView; + PMSIHANDLE hRecord; + CFileOperations::FILE_ENTRY rootFileEntry; + LPWSTR* pszReparsePoints = nullptr; + UINT cReparsePoints = 0; + CReparsePoint execCAD; + CReparsePoint rollbackCAD; + + hr = WcaInitialize(hInstall, __FUNCTION__); + ExitOnFailure(hr, "Failed to initialize"); + WcaLog(LOGMSG_STANDARD, "Initialized from PanelSwCustomActions " FullVersion); + + // Ensure table PSW_RemoveFolderEx exists. + hr = WcaTableExists(L"RemoveFile"); + ExitOnFailure(hr, "Failed to check if table exists 'RemoveFile'"); + ExitOnNull((hr == S_OK), hr, E_FAIL, "Table does not exist 'RemoveFile'. Have you authored 'PanelSw:RemoveFolderEx' entries in WiX code?"); + + // Execute view + hr = WcaOpenExecuteView(L"SELECT `Component_`, `FileName`, `DirProperty`, `InstallMode` FROM `RemoveFile`", &hView); + ExitOnFailure(hr, "Failed to execute SQL query"); + + // Iterate records + while ((hr = WcaFetchRecord(hView, &hRecord)) != E_NOMOREITEMS) + { + ExitOnFailure(hr, "Failed to fetch record."); + CFileOperations::ReleaseFileEntries(&rootFileEntry); + ReleaseNullStrArray(pszReparsePoints, cReparsePoints); + + // Get fields + CWixString szComponent, szFileName, szDirProperty; + RemoveFileInstallMode flags = RemoveFileInstallMode::RemoveFileInstallMode_Unknown; + WCA_TODO componentAction = WCA_TODO::WCA_TODO_UNKNOWN; + + hr = WcaGetRecordString(hRecord, 1, (LPWSTR*)szComponent); + ExitOnFailure(hr, "Failed to get Component_."); + hr = WcaGetRecordString(hRecord, 2, (LPWSTR*)szFileName); + ExitOnFailure(hr, "Failed to get FileName."); + hr = WcaGetRecordString(hRecord, 3, (LPWSTR*)szDirProperty); + ExitOnFailure(hr, "Failed to get DirProperty."); + hr = WcaGetRecordInteger(hRecord, 4, (int*)&flags); + ExitOnFailure(hr, "Failed to get InstallMode."); + + // Workaround WiX bug in v4- Adds a short name to RemoveFile/@FileName. + if (!szFileName.IsNullOrEmpty()) + { + DWORD longNameIdx = szFileName.Find(L'|'); + if (longNameIdx < szFileName.StrLen()) + { + CWixString szOrig(szFileName); + szFileName.RemoveLeft(longNameIdx + 1); + WcaLog(LOGLEVEL::LOGMSG_STANDARD, "RemoveFile/@FileName column contains a short and long file names '%ls'. Ignoring the short name and using '%ls' only", (LPCWSTR)szOrig, (LPCWSTR)szFileName); + } + } + + componentAction = WcaGetComponentToDo(szComponent); + if ((componentAction != WCA_TODO_INSTALL) && (componentAction != WCA_TODO_REINSTALL) && (componentAction != WCA_TODO_UNINSTALL)) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveReparseData for property '%ls' because component action isn't compatible", (LPCWSTR)szDirProperty); + continue; + } + if (((componentAction == WCA_TODO_INSTALL) || (componentAction == WCA_TODO_REINSTALL)) && !(flags & RemoveFileInstallMode_Install)) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveReparseData for property '%ls' because component action is (re)install", (LPCWSTR)szDirProperty); + continue; + } + if ((componentAction == WCA_TODO_UNINSTALL) && !(flags & RemoveFileInstallMode_Uninstall)) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveReparseData for property '%ls' because component action is uninstall", (LPCWSTR)szDirProperty); + continue; + } + + hr = WcaGetProperty(szDirProperty, &rootFileEntry.szPath); + ExitOnFailure(hr, "Failed to get property"); + + if (!rootFileEntry.szPath || !*rootFileEntry.szPath) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveReparseData for property '%ls' because it is empty", (LPCWSTR)szDirProperty); + continue; + } + + rootFileEntry.dwAttributes = GetFileAttributesW(rootFileEntry.szPath); + if (rootFileEntry.dwAttributes == INVALID_FILE_ATTRIBUTES) + { + WcaLogError(HRESULT_FROM_WIN32(::GetLastError()), "Failed to get file attributes for '%ls' because it is empty. Skipping RemoveReparseData for it", rootFileEntry.szPath); + continue; + } + if ((rootFileEntry.dwAttributes & FILE_ATTRIBUTE_DIRECTORY) != FILE_ATTRIBUTE_DIRECTORY) + { + CDeferredActionBase::LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, false, L"Skipping RemoveReparseData for property '%ls' because it is not a folder", rootFileEntry.szPath); + continue; + } + + if (!szFileName.IsNullOrEmpty()) + { + hr = CFileOperations::ListFileEntries(&rootFileEntry, szFileName, false); + ExitOnFailure(hr, "Failed to list file entries under '%ls'", rootFileEntry.szPath); + } + + hr = CFileOperations::FilterFileEntries(&rootFileEntry, FILE_ATTRIBUTE_REPARSE_POINT, 0, &pszReparsePoints, &cReparsePoints); + ExitOnFailure(hr, "Failed to filter file entries with reparse points under '%ls'", rootFileEntry.szPath); + + for (UINT i = 0; i < cReparsePoints; ++i) + { + hr = rollbackCAD.AddRestoreReparsePoint(pszReparsePoints[i]); + ExitOnFailure(hr, "Failed to get reparse point data for '%ls'", pszReparsePoints[i]); + + hr = execCAD.AddDeleteReparsePoint(pszReparsePoints[i]); + ExitOnFailure(hr, "Failed to add exec data for reparse point of '%ls'", pszReparsePoints[i]); + } + } + hr = S_OK; + + hr = rollbackCAD.DoDeferredAction(L"RemoveReparseDataRollback"); + ExitOnFailure(hr, "Failed to do action"); + + hr = execCAD.DoDeferredAction(L"RemoveReparseDataExec"); + ExitOnFailure(hr, "Failed to do action"); + +LExit: + CFileOperations::ReleaseFileEntries(&rootFileEntry); + ReleaseNullStrArray(pszReparsePoints, cReparsePoints); + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +HRESULT CReparsePoint::AddRestoreReparsePoint(LPCWSTR szPath) +{ + HRESULT hr = S_OK; + ::com::panelsw::ca::Command *pCmd = nullptr; + ReparsePointDetails *pDetails = nullptr; + ::std::string *pAny = nullptr; + bool bRes = true; + void* pBuffer = nullptr; + DWORD dwSize = 0; + + hr = AddCommand("CReparsePoint", &pCmd); + ExitOnFailure(hr, "Failed to add command"); + + pDetails = new ReparsePointDetails(); + ExitOnNull(pDetails, hr, E_FAIL, "Failed allocating details"); + + hr = GetReparsePointData(szPath, &pBuffer, &dwSize); + ExitOnFailure(hr, "Failed to read reparse point data for '%ls'", szPath); + + pDetails->set_action(::com::panelsw::ca::ReparsePointAction::restore); + pDetails->set_path(szPath, WSTR_BYTE_SIZE(szPath)); + pDetails->set_reparsedata(pBuffer, dwSize); + + pAny = pCmd->mutable_details(); + ExitOnNull(pAny, hr, E_FAIL, "Failed allocating any"); + + bRes = pDetails->SerializeToString(pAny); + ExitOnNull(bRes, hr, E_FAIL, "Failed serializing command details"); + +LExit: + ReleaseMem(pBuffer); + + return hr; +} + +HRESULT CReparsePoint::AddDeleteReparsePoint(LPCWSTR szPath) +{ + HRESULT hr = S_OK; + ::com::panelsw::ca::Command* pCmd = nullptr; + ReparsePointDetails* pDetails = nullptr; + ::std::string* pAny = nullptr; + bool bRes = true; + void* pBuffer = nullptr; + DWORD dwSize = 0; + + hr = AddCommand("CReparsePoint", &pCmd); + ExitOnFailure(hr, "Failed to add command"); + + pDetails = new ReparsePointDetails(); + ExitOnNull(pDetails, hr, E_FAIL, "Failed allocating details"); + + hr = GetReparsePointData(szPath, &pBuffer, &dwSize); + ExitOnFailure(hr, "Failed to read reparse point data for '%ls'", szPath); + + pDetails->set_action(::com::panelsw::ca::ReparsePointAction::delete_); + pDetails->set_path(szPath, WSTR_BYTE_SIZE(szPath)); + pDetails->set_reparsedata(pBuffer, dwSize); + + pAny = pCmd->mutable_details(); + ExitOnNull(pAny, hr, E_FAIL, "Failed allocating any"); + + bRes = pDetails->SerializeToString(pAny); + ExitOnNull(bRes, hr, E_FAIL, "Failed serializing command details"); + +LExit: + ReleaseMem(pBuffer); + + return hr; +} + +HRESULT CReparsePoint::DeferredExecute(const ::std::string& command) +{ + HRESULT hr = S_OK; + BOOL bRes = TRUE; + ReparsePointDetails details; + LPCWSTR szPath = nullptr; + LPVOID pData = nullptr; + DWORD dwSize = 0; + + bRes = details.ParseFromString(command); + ExitOnNull(bRes, hr, E_INVALIDARG, "Failed unpacking ReparsePointDetails"); + + szPath = (LPCWSTR)details.path().c_str(); + pData = const_cast(details.reparsedata().data()); + dwSize = details.reparsedata().size(); + + switch (details.action()) + { + case ::com::panelsw::ca::ReparsePointAction::delete_: + WcaLog(LOGLEVEL::LOGMSG_STANDARD, "Removing reparse point data from '%ls'", szPath); + hr = DeleteReparsePoint(szPath, pData, dwSize); + ExitOnFailure(hr, "Failed to delete reparse point in '%ls'", szPath); + break; + case ::com::panelsw::ca::ReparsePointAction::restore: + WcaLog(LOGLEVEL::LOGMSG_STANDARD, "Restoring reparse point data to '%ls'", szPath); + hr = CreateReparsePoint(szPath, pData, dwSize); + ExitOnFailure(hr, "Failed to set reparse point in '%ls'", szPath); + break; + default: + hr = E_INVALIDDATA; + ExitOnFailure(hr, "Illega reparse point action %i requested for '%ls'", details.action(), (LPCWSTR)details.path().c_str()); + break; + } + +LExit: + return hr; +} + +/*static*/ bool CReparsePoint::IsSymbolicLinkOrMount(LPCWSTR szPath) +{ + HANDLE hFind = INVALID_HANDLE_VALUE; + WIN32_FIND_DATA wfaData; + HRESULT hr = S_OK; + + hFind = ::FindFirstFile(szPath, &wfaData); + if (hFind == INVALID_HANDLE_VALUE) + { + hr = S_FALSE; + DWORD dwErr = ::GetLastError(); + ExitOnNullWithLastError(((dwErr == ERROR_FILE_NOT_FOUND) || (dwErr == ERROR_PATH_NOT_FOUND)), hr, "Path '%ls' can't be checked for reparse point tag", szPath); + } + +LExit: + ReleaseFileFindHandle(hFind); + + return (hr == S_OK) ? IsSymbolicLinkOrMount(&wfaData) : false; +} + +/*static*/ bool CReparsePoint::IsSymbolicLinkOrMount(const WIN32_FIND_DATA* pFindFileData) +{ + return ((pFindFileData->dwFileAttributes != INVALID_FILE_ATTRIBUTES) + && (((pFindFileData->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT) + && ((pFindFileData->dwReserved0 == IO_REPARSE_TAG_SYMLINK) || (pFindFileData->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT)))); +} + +HRESULT CReparsePoint::GetReparsePointData(LPCWSTR szPath, void** ppBuffer, DWORD* pdwSize) +{ + HRESULT hr = S_OK; + BOOL bRes = TRUE; + HANDLE hFile = INVALID_HANDLE_VALUE; + void* pBuffer = nullptr; + DWORD dwReparseDataSize = 0; + DWORD dwBufferSize = 0; + DWORD dwFlags = FILE_FLAG_OPEN_REPARSE_POINT; + REPARSE_DATA_COMMON_HEADER* pReparseCommon = nullptr; + + if (::PathIsDirectory(szPath)) + { + dwFlags |= FILE_FLAG_BACKUP_SEMANTICS; + } + + hFile = ::CreateFile(szPath, GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, dwFlags, NULL); + ExitOnInvalidHandleWithLastError(hFile, hr, "Failed to open file '%ls'", szPath); + + dwBufferSize = MAX_PATH; + do + { + ReleaseNullMem(pBuffer); + dwReparseDataSize = 0; + + dwBufferSize *= 2; + pBuffer = MemAlloc(dwBufferSize, TRUE); + ExitOnNull(pBuffer, hr, E_OUTOFMEMORY, "Failed to allocate buffer"); + + bRes = ::DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, nullptr, 0, pBuffer, dwBufferSize, &dwReparseDataSize, nullptr); + + } while (!bRes && (::GetLastError() == ERROR_INSUFFICIENT_BUFFER)); + ExitOnNullWithLastError(bRes, hr, "Failed to get reparse point data of '%ls'", szPath); + + if (dwReparseDataSize) + { + ExitOnNull(dwReparseDataSize >= sizeof(REPARSE_DATA_COMMON_HEADER), hr, E_INVALIDDATA, "Reparse data size is lesser than header size"); + REPARSE_DATA_COMMON_HEADER* pReparseCommon = (REPARSE_DATA_COMMON_HEADER*)pBuffer; + + if (IsReparseTagMicrosoft(pReparseCommon->ReparseTag)) + { + ExitOnNull((dwReparseDataSize >= REPARSE_DATA_BUFFER_HEADER_SIZE), hr, E_INVALIDDATA, "Reparse data size is lesser than MS header size"); + } + else + { + ExitOnNull((dwReparseDataSize >= REPARSE_GUID_DATA_BUFFER_HEADER_SIZE), hr, E_INVALIDDATA, "Reparse data size is lesser than GUID header size"); + } + + *ppBuffer = pBuffer; + *pdwSize = dwReparseDataSize; + pBuffer = nullptr; + } + +LExit: + ReleaseFile(hFile); + ReleaseMem(pBuffer); + + return hr; +} + +HRESULT CReparsePoint::CreateReparsePoint(LPCWSTR szPath, LPVOID pBuffer, DWORD dwSize) +{ + HRESULT hr = S_OK; + BOOL bRes = TRUE; + HANDLE hFile = INVALID_HANDLE_VALUE; + DWORD dwFlags = FILE_FLAG_OPEN_REPARSE_POINT; + + if (::PathIsDirectory(szPath)) + { + dwFlags |= FILE_FLAG_BACKUP_SEMANTICS; + } + + hFile = ::CreateFile(szPath, GENERIC_ALL, FILE_SHARE_READ, nullptr, OPEN_EXISTING, dwFlags, NULL); + ExitOnInvalidHandleWithLastError(hFile, hr, "Failed to open file '%ls'", szPath); + + bRes = ::DeviceIoControl(hFile, FSCTL_SET_REPARSE_POINT, pBuffer, dwSize, nullptr, 0, nullptr, nullptr); + ExitOnNullWithLastError(bRes, hr, "Failed to set reparse point data of '%ls'", szPath); + +LExit: + ReleaseFile(hFile); + + return hr; +} + +HRESULT CReparsePoint::DeleteReparsePoint(LPCWSTR szPath, LPVOID pBuffer, DWORD dwSize) +{ + HRESULT hr = S_OK; + BOOL bRes = TRUE; + HANDLE hFile = INVALID_HANDLE_VALUE; + DWORD dwFlags = FILE_FLAG_OPEN_REPARSE_POINT; + REPARSE_DATA_COMMON_HEADER *pReparseCommon = nullptr; + void* pCurrReparseData = nullptr; + DWORD cCurrReparseData = 0; + + // Check if reparse data was already deleted, as may happen if multiple RemoveFile entries match on the same file + GetReparsePointData(szPath, &pCurrReparseData, &cCurrReparseData); + if (cCurrReparseData == 0) + { + hr = S_FALSE; + WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Reparse data for '%ls' already removed", szPath); + ExitFunction(); + } + + if (::PathIsDirectory(szPath)) + { + dwFlags |= FILE_FLAG_BACKUP_SEMANTICS; + } + + pReparseCommon = (REPARSE_DATA_COMMON_HEADER*)pBuffer; + pReparseCommon->ReparseDataLength = 0; + if (IsReparseTagMicrosoft(pReparseCommon->ReparseTag)) + { + dwSize = REPARSE_DATA_BUFFER_HEADER_SIZE; + } + else + { + dwSize = REPARSE_GUID_DATA_BUFFER_HEADER_SIZE; + } + + hFile = ::CreateFile(szPath, GENERIC_ALL, FILE_SHARE_READ, nullptr, OPEN_EXISTING, dwFlags, NULL); + ExitOnInvalidHandleWithLastError(hFile, hr, "Failed to open file '%ls'", szPath); + + bRes = ::DeviceIoControl(hFile, FSCTL_DELETE_REPARSE_POINT, pBuffer, dwSize, nullptr, 0, nullptr, nullptr); + ExitOnNullWithLastError(bRes, hr, "Failed to delete reparse point data of '%ls'", szPath); + +LExit: + ReleaseFile(hFile); + + return hr; +} diff --git a/src/PanelSwCustomActions/ReparsePoint.h b/src/PanelSwCustomActions/ReparsePoint.h new file mode 100644 index 00000000..5e1a86f7 --- /dev/null +++ b/src/PanelSwCustomActions/ReparsePoint.h @@ -0,0 +1,30 @@ +#pragma once +#include "../CaCommon/DeferredActionBase.h" + +class CReparsePoint : + public CDeferredActionBase +{ +public: + + CReparsePoint() : CDeferredActionBase("ReparsePoint") { } + + HRESULT AddRestoreReparsePoint(LPCWSTR szPath); + + HRESULT AddDeleteReparsePoint(LPCWSTR szPath); + + static bool IsSymbolicLinkOrMount(LPCWSTR szPath); + static bool IsSymbolicLinkOrMount(const WIN32_FIND_DATA *pFindFileData); + +protected: + + HRESULT DeferredExecute(const ::std::string& command) override; + +private: + + HRESULT DeleteReparsePoint(LPCWSTR szPath, LPVOID pBuffer, DWORD dwSize); + + HRESULT CreateReparsePoint(LPCWSTR szPath, LPVOID pBuffer, DWORD dwSize); + + HRESULT GetReparsePointData(LPCWSTR szPath, void** ppBuffer, DWORD* pdwSize); +}; + diff --git a/src/PanelSwWixExtension/PanelSwWiBackendBinder.cs b/src/PanelSwWixExtension/PanelSwWiBackendBinder.cs index 298bf090..76f29b29 100644 --- a/src/PanelSwWixExtension/PanelSwWiBackendBinder.cs +++ b/src/PanelSwWixExtension/PanelSwWiBackendBinder.cs @@ -500,6 +500,7 @@ public override IReadOnlyCollection TableDefinitions new TableDefinition(nameof(PSW_Payload), PSW_Payload.SymbolDefinition, PSW_Payload.ColumnDefinitions, symbolIdIsPrimaryKey: false), new TableDefinition(nameof(PSW_ReadIniValues), PSW_ReadIniValues.SymbolDefinition, PSW_ReadIniValues.ColumnDefinitions, symbolIdIsPrimaryKey: true), new TableDefinition(nameof(PSW_RegularExpression), PSW_RegularExpression.SymbolDefinition, PSW_RegularExpression.ColumnDefinitions, symbolIdIsPrimaryKey: true), + new TableDefinition(nameof(PSW_RemoveFolderEx), PSW_RemoveFolderEx.SymbolDefinition, PSW_RemoveFolderEx.ColumnDefinitions, symbolIdIsPrimaryKey: true), new TableDefinition(nameof(PSW_RemoveRegistryValue), PSW_RemoveRegistryValue.SymbolDefinition, PSW_RemoveRegistryValue.ColumnDefinitions, symbolIdIsPrimaryKey: true), new TableDefinition(nameof(PSW_RestartLocalResources), PSW_RestartLocalResources.SymbolDefinition, PSW_RestartLocalResources.ColumnDefinitions, symbolIdIsPrimaryKey: true), new TableDefinition(nameof(PSW_SelfSignCertificate), PSW_SelfSignCertificate.SymbolDefinition, PSW_SelfSignCertificate.ColumnDefinitions, symbolIdIsPrimaryKey: true), diff --git a/src/PanelSwWixExtension/PanelSwWixCompiler.cs b/src/PanelSwWixExtension/PanelSwWixCompiler.cs index e9edebc0..d1b05f4a 100644 --- a/src/PanelSwWixExtension/PanelSwWixCompiler.cs +++ b/src/PanelSwWixExtension/PanelSwWixCompiler.cs @@ -260,7 +260,9 @@ public override void ParseElement(Intermediate intermediate, IntermediateSection case "XslTransform": ParseXslTransform(section, element, componentId, null); break; - + case "RemoveFolderEx": + ParseRemoveFolderEx(section, element, componentId); + break; default: ParseHelper.UnexpectedElement(parentElement, element); break; @@ -330,6 +332,48 @@ public override void ParseElement(Intermediate intermediate, IntermediateSection } } + private void ParseRemoveFolderEx(IntermediateSection section, XElement element, string componentId) + { + SourceLineNumber sourceLineNumbers = ParseHelper.GetSourceLineNumbers(element); + string property = null; + PSW_RemoveFolderEx.RemoveFolderExInstallMode on = PSW_RemoveFolderEx.RemoveFolderExInstallMode.Uninstall; + + foreach (XAttribute attrib in element.Attributes()) + { + if (IsMyAttribute(element, attrib)) + { + switch (attrib.Name.LocalName) + { + case "Property": + property = ParseHelper.GetAttributeIdentifierValue(sourceLineNumbers, attrib); + break; + case "On": + TryParseEnumAttribute(sourceLineNumbers, element, attrib, out on); + break; + default: + ParseHelper.UnexpectedAttribute(element, attrib); + break; + } + } + } + + if (string.IsNullOrEmpty(property)) + { + Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, element.Name.LocalName, "Property")); + } + + if (!Messaging.EncounteredError) + { + ParseHelper.CreateSimpleReference(section, sourceLineNumbers, "CustomAction", "PSW_RemoveFolderEx"); + ParseHelper.EnsureTable(section, sourceLineNumbers, "RemoveFile"); + + PSW_RemoveFolderEx symbol = section.AddSymbol(new PSW_RemoveFolderEx(sourceLineNumbers)); + symbol.Component_ = componentId; + symbol.Property = property; + symbol.InstallMode = on; + } + } + private void ParseCustomSearchElement(IntermediateSection section, XElement element) { SourceLineNumber sourceLineNumbers = ParseHelper.GetSourceLineNumbers(element); diff --git a/src/PanelSwWixExtension/Symbols/PSW_RemoveFolderEx.cs b/src/PanelSwWixExtension/Symbols/PSW_RemoveFolderEx.cs new file mode 100644 index 00000000..11ab5982 --- /dev/null +++ b/src/PanelSwWixExtension/Symbols/PSW_RemoveFolderEx.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using WixToolset.Data; +using WixToolset.Data.WindowsInstaller; + +namespace PanelSw.Wix.Extensions.Symbols +{ + internal class PSW_RemoveFolderEx : BaseSymbol + { + public enum RemoveFolderExInstallMode + { + Install = 1, + Uninstall = 2, + Both = 3, + } + + public static IntermediateSymbolDefinition SymbolDefinition + { + get + { + return new IntermediateSymbolDefinition(nameof(PSW_RemoveFolderEx), CreateFieldDefinitions(ColumnDefinitions), typeof(PSW_RemoveFolderEx)); + } + } + public static IEnumerable ColumnDefinitions + { + get + { + return new ColumnDefinition[] + { + new ColumnDefinition(nameof(Id), ColumnType.String, 72, true, false, ColumnCategory.Identifier, modularizeType: ColumnModularizeType.Column), + new ColumnDefinition(nameof(Component_), ColumnType.String, 0, false, false, ColumnCategory.Text, modularizeType: ColumnModularizeType.Column), + new ColumnDefinition(nameof(Property), ColumnType.String, 0, false, false, ColumnCategory.Text, modularizeType: ColumnModularizeType.Column), + new ColumnDefinition(nameof(InstallMode), ColumnType.Number, 0, false, false, ColumnCategory.Integer, modularizeType: ColumnModularizeType.None), + }; + } + } + + public PSW_RemoveFolderEx() : base(SymbolDefinition) + { } + + public PSW_RemoveFolderEx(SourceLineNumber lineNumber) : base(SymbolDefinition, lineNumber, "rmf") + { } + + public string Component_ + { + get => Fields[0].AsString(); + set => this.Set(0, value); + } + + public string Property + { + get => Fields[1].AsString(); + set => this.Set(1, value); + } + + public RemoveFolderExInstallMode InstallMode + { + get => (RemoveFolderExInstallMode)Fields[2].AsNumber(); + set => this.Set(2, (int)value); + } + } +} diff --git a/src/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd b/src/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd index 3b3b996d..996df2cd 100644 --- a/src/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd +++ b/src/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd @@ -32,6 +32,38 @@ + + + + + + + + + + + + + + + Property that hold folder path to remove entirely. This field may be formatted, but is resolved before CostInitialize, so it may not be a Directory table key. + + + + + + This value determines the time at which the folder may be removed, based on the install/uninstall of the parent component. + For 'install', the folder will be removed only when the parent component is being installed (msiInstallStateLocal or + msiInstallStateSource); for 'uninstall', the folder will be removed only when the parent component + is being removed (msiInstallStateAbsent); for 'both', the folder will be removed in both cases. + + + + + + + + diff --git a/src/ProtoCaLib/ProtoCaLib.vcxproj b/src/ProtoCaLib/ProtoCaLib.vcxproj index 30014f1e..6c316f53 100644 --- a/src/ProtoCaLib/ProtoCaLib.vcxproj +++ b/src/ProtoCaLib/ProtoCaLib.vcxproj @@ -23,7 +23,7 @@ Static true .lib - false + false @@ -120,6 +120,9 @@ True + + True + @@ -179,6 +182,9 @@ True + + True + @@ -189,6 +195,7 @@ + @@ -202,7 +209,7 @@ - + diff --git a/src/ProtoCaLib/ProtoCaLib.vcxproj.filters b/src/ProtoCaLib/ProtoCaLib.vcxproj.filters index 7a685196..eb0bec19 100644 --- a/src/ProtoCaLib/ProtoCaLib.vcxproj.filters +++ b/src/ProtoCaLib/ProtoCaLib.vcxproj.filters @@ -76,6 +76,9 @@ Source Files + + Source Files + @@ -135,9 +138,11 @@ Header Files + + Header Files + - Protobuf @@ -195,6 +200,9 @@ Protobuf + + Protobuf + diff --git a/src/ProtoCaLib/reparsePointDetails.proto b/src/ProtoCaLib/reparsePointDetails.proto new file mode 100644 index 00000000..a0784472 --- /dev/null +++ b/src/ProtoCaLib/reparsePointDetails.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; +package com.panelsw.ca; +option optimize_for = LITE_RUNTIME; + +enum ReparsePointAction { + delete = 0; + restore = 1; +} + +message ReparsePointDetails { + ReparsePointAction action = 1; + bytes path = 2; + bytes reparseData = 3; +} diff --git a/src/README.md b/src/README.md index 9395dbc1..dc90bc38 100644 --- a/src/README.md +++ b/src/README.md @@ -43,6 +43,7 @@ I would like to thank JetBrains for their [support](https://www.jetbrains.com/co - *Payload* Support extracting files from Binary table temporarilty during MSI execution - *PromptFileDowngrades* Log each file that will be downgraded during the (re)install, and prompt the total downgrade count if larger than 0. - Deferred Actions: + - *RemoveFolderEx*: Remove a folder recursively. Differs from WixUtilExtension's RemoveFolderEx in proper handling of reparse points (symbolic links, mount volumes, etc.) - *ExecuteCommand*: Launch a deferred command - *RestartLocalResources*: Register processes with the Restart Manager if they reside in the specified folder - *XslTransform*: Apply a XSL transform on an installed XML file diff --git a/src/TidyBuild.custom.props b/src/TidyBuild.custom.props index 2269b3d7..6f4bbe85 100644 --- a/src/TidyBuild.custom.props +++ b/src/TidyBuild.custom.props @@ -2,7 +2,7 @@ - 3.20.1 + 3.21.0 $(FullVersion).$(GITHUB_RUN_NUMBER) PanelSwWixExtension Panel::Software diff --git a/src/UnitTests/FileOperationsUT/FileOperationsUT.wixproj b/src/UnitTests/FileOperationsUT/FileOperationsUT.wixproj index 10a99426..facca254 100644 --- a/src/UnitTests/FileOperationsUT/FileOperationsUT.wixproj +++ b/src/UnitTests/FileOperationsUT/FileOperationsUT.wixproj @@ -9,6 +9,9 @@ + + + diff --git a/src/UnitTests/FileOperationsUT/FileOperationsUT.wxs b/src/UnitTests/FileOperationsUT/FileOperationsUT.wxs index ca5fd5c1..1e37e46a 100644 --- a/src/UnitTests/FileOperationsUT/FileOperationsUT.wxs +++ b/src/UnitTests/FileOperationsUT/FileOperationsUT.wxs @@ -20,6 +20,22 @@ + + + + + + + + + + + + + + + + @@ -29,5 +45,9 @@ + + + + diff --git a/src/UnitTests/FileOperationsUT/run-test.bat b/src/UnitTests/FileOperationsUT/run-test.bat new file mode 100644 index 00000000..c887afc0 --- /dev/null +++ b/src/UnitTests/FileOperationsUT/run-test.bat @@ -0,0 +1,188 @@ +ECHO OFF +SET /A MY_ERR=0 + +:: Install +CALL :prepareFolders +msiexec /i FileOperationsUT.msi /l*v FileOperationsUT.msi.log REMOVE_ON_INSTALL="%CD%\install-remove" REMOVE_ON_REPAIR="%CD%\repair-remove" REMOVE_ON_UNINSTALL="%CD%\uninstall-remove" REMOVE_ON_BOTH="%CD%\both-remove" NEVER_REMOVED="%CD%\never-remove" +CALL :testInstall +PAUSE + +:: Repair +CALL :prepareFolders +msiexec /fvamus FileOperationsUT.msi /l*v FileOperationsUT.msif.log REMOVE_ON_INSTALL="%CD%\install-remove" REMOVE_ON_REPAIR="%CD%\repair-remove" REMOVE_ON_UNINSTALL="%CD%\uninstall-remove" REMOVE_ON_BOTH="%CD%\both-remove" NEVER_REMOVED="%CD%\never-remove" +CALL :testRepair +PAUSE + +:: Uninstall +CALL :prepareFolders +msiexec /xFileOperationsUT.msi /l*v FileOperationsUT.msix.log REMOVE_ON_INSTALL="%CD%\install-remove" REMOVE_ON_REPAIR="%CD%\repair-remove" REMOVE_ON_UNINSTALL="%CD%\uninstall-remove" REMOVE_ON_BOTH="%CD%\both-remove" NEVER_REMOVED="%CD%\never-remove" +CALL :testUninstall +PAUSE + +:: Clean and exit +CALL :cleanFolders +ECHO Overall error code %MY_ERR% +EXIT /B %MY_ERR% + +:prepareFolders + CALL :cleanFolders + + MKDIR "%CD%\d-target" + ECHO test > "%CD%\d-target\f-target.txt" + + :: Folder install-remove should be removed entirely on install + MKDIR "%CD%\install-remove\2\3" + ECHO test > "%CD%\install-remove\2\file.txt" + ECHO test > "%CD%\install-remove\2\3\file.txt" + mklink /D "%CD%\install-remove\d-sl-1" "%CD%\d-target" + mklink /D "%CD%\install-remove\2\3\d-sl-2" "%CD%\d-target" + mklink "%CD%\install-remove\f-sl-1.txt" "%CD%\d-target\f-target.txt" + mklink "%CD%\install-remove\2\3\f-sl-2.txt" "%CD%\d-target\f-target.txt" + + :: Folder repair-remove should be removed entirely on repair + MKDIR "%CD%\repair-remove\2\3" + ECHO test > "%CD%\repair-remove\2\file.txt" + ECHO test > "%CD%\repair-remove\2\3\file.txt" + mklink /D "%CD%\repair-remove\d-sl-1" "%CD%\d-target" + mklink /D "%CD%\repair-remove\2\3\d-sl-2" "%CD%\d-target" + mklink "%CD%\repair-remove\f-sl-1.txt" "%CD%\d-target\f-target.txt" + mklink "%CD%\repair-remove\2\3\f-sl-2.txt" "%CD%\d-target\f-target.txt" + + :: Folder uninstall-remove should be removed entirely on uninstall + MKDIR "%CD%\uninstall-remove\2\3" + ECHO test > "%CD%\uninstall-remove\2\file.txt" + ECHO test > "%CD%\uninstall-remove\2\3\file.txt" + mklink /D "%CD%\uninstall-remove\d-sl-1" "%CD%\d-target" + mklink /D "%CD%\uninstall-remove\2\3\d-sl-2" "%CD%\d-target" + mklink "%CD%\uninstall-remove\f-sl-1.txt" "%CD%\d-target\f-target.txt" + mklink "%CD%\uninstall-remove\2\3\f-sl-2.txt" "%CD%\d-target\f-target.txt" + + :: Folder both-remove should be removed entirely on both install and uninstall + MKDIR "%CD%\both-remove\2\3" + ECHO test > "%CD%\both-remove\2\file.txt" + ECHO test > "%CD%\both-remove\2\3\file.txt" + mklink /D "%CD%\both-remove\d-sl-1" "%CD%\d-target" + mklink /D "%CD%\both-remove\2\3\d-sl-2" "%CD%\d-target" + mklink "%CD%\both-remove\f-sl-1.txt" "%CD%\d-target\f-target.txt" + mklink "%CD%\both-remove\2\3\f-sl-2.txt" "%CD%\d-target\f-target.txt" + + :: Folder "%CD%\never-remove" and all content should never be removed + MKDIR "%CD%\never-remove" + ECHO test > "%CD%\never-remove\file.txt" + mklink /D "%CD%\never-remove\d-sl-2" "%CD%\d-target" + mklink "%CD%\never-remove\f-sl-1.txt" "%CD%\d-target\f-target.txt" +EXIT /B %MY_ERR% + +:cleanFolders + RMDIR /s /q "%CD%\d-target" + RMDIR /s /q "%CD%\install-remove" + RMDIR /s /q "%CD%\repair-remove" + RMDIR /s /q "%CD%\uninstall-remove" + RMDIR /s /q "%CD%\both-remove" + RMDIR /s /q "%CD%\never-remove" +EXIT /B %MY_ERR% + +:testInstall + IF EXIST "%CD%\install-remove\" ( + ECHO Folder "%CD%\install-remove\" should not exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\repair-remove\" ( + ECHO Folder "%CD%\repair-remove\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\uninstall-remove\" ( + ECHO Folder "%CD%\uninstall-remove\" should exist + SET /A MY_ERR=1 + ) + IF EXIST "%CD%\both-remove\" ( + ECHO Folder "%CD%\both-remove\" should not exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\file.txt" ( + ECHO File "%CD%\never-remove\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\d-sl-2\" ( + ECHO Link "%CD%\never-remove\d-sl-2\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\f-sl-1.txt" ( + ECHO Link "%CD%\never-remove\f-sl-1.txt" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\" ( + ECHO Folder "%CD%\both-remove\" should exist + SET /A MY_ERR=1 + ) +EXIT /B %MY_ERR% + +:testRepair + IF EXIST "%CD%\install-remove\" ( + ECHO Folder "%CD%\install-remove\" should not exist + SET /A MY_ERR=1 + ) + IF EXIST "%CD%\repair-remove\" ( + ECHO Folder "%CD%\repair-remove\" should not exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\uninstall-remove\" ( + ECHO Folder "%CD%\uninstall-remove\" should exist + SET /A MY_ERR=1 + ) + IF EXIST "%CD%\both-remove\" ( + ECHO Folder "%CD%\both-remove\" should not exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\file.txt" ( + ECHO File "%CD%\never-remove\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\d-sl-2\" ( + ECHO Link "%CD%\never-remove\d-sl-2\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\f-sl-1.txt" ( + ECHO Link "%CD%\never-remove\f-sl-1.txt" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\" ( + ECHO Folder "%CD%\both-remove\" should exist + SET /A MY_ERR=1 + ) +EXIT /B %MY_ERR% + +:testUninstall + IF NOT EXIST "%CD%\install-remove\" ( + ECHO Folder "%CD%\install-remove\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\repair-remove\" ( + ECHO Folder "%CD%\repair-remove\" should exist + SET /A MY_ERR=1 + ) + IF EXIST "%CD%\uninstall-remove\" ( + ECHO Folder "%CD%\uninstall-remove\" should not exist + SET /A MY_ERR=1 + ) + IF EXIST "%CD%\both-remove\" ( + ECHO Folder "%CD%\both-remove\" should not exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\file.txt" ( + ECHO File "%CD%\never-remove\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\d-sl-2\" ( + ECHO Link "%CD%\never-remove\d-sl-2\" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\f-sl-1.txt" ( + ECHO Link "%CD%\never-remove\f-sl-1.txt" should exist + SET /A MY_ERR=1 + ) + IF NOT EXIST "%CD%\never-remove\" ( + ECHO Folder "%CD%\both-remove\" should exist + SET /A MY_ERR=1 + ) +EXIT /B %MY_ERR% diff --git a/src/wixlib/PanelSwWixExtension.wxs b/src/wixlib/PanelSwWixExtension.wxs index a3fa91b7..00436044 100644 --- a/src/wixlib/PanelSwWixExtension.wxs +++ b/src/wixlib/PanelSwWixExtension.wxs @@ -938,4 +938,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/wixlib/en-US.wxl b/src/wixlib/en-US.wxl index 71791276..3cd1da9e 100644 --- a/src/wixlib/en-US.wxl +++ b/src/wixlib/en-US.wxl @@ -62,4 +62,6 @@ + + diff --git a/src/wixlib/he-IL.wxl b/src/wixlib/he-IL.wxl index e67608bd..728b6ae8 100644 --- a/src/wixlib/he-IL.wxl +++ b/src/wixlib/he-IL.wxl @@ -62,4 +62,6 @@ + +