From 0f5c4393541b4dc029a22b7304dd4dcb7b2bc481 Mon Sep 17 00:00:00 2001 From: Nir Bar Date: Tue, 12 Nov 2024 14:08:09 +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 --- PanelSwCustomActions/CommonDeferred.cpp | 6 + PanelSwCustomActions/FileOperations.cpp | 398 ++++++++++----- PanelSwCustomActions/FileOperations.h | 25 +- PanelSwCustomActions/PanelSwCustomActions.def | 2 + .../PanelSwCustomActions.vcxproj | 4 +- .../PanelSwCustomActions.vcxproj.filters | 5 +- PanelSwCustomActions/ReparsePoint.cpp | 461 ++++++++++++++++++ PanelSwCustomActions/ReparsePoint.h | 30 ++ PanelSwCustomActions/SqlScript.cpp | 5 +- PanelSwWixExtension/Data/tables.xml | 11 +- PanelSwWixExtension/PanelSwWixCompiler.cs | 62 +++ .../Xsd/PanelSwWixExtension.xsd | 32 ++ ProtoCaLib/ProtoCaLib.vcxproj | 7 + ProtoCaLib/ProtoCaLib.vcxproj.filters | 11 +- ProtoCaLib/reparsePointDetails.proto | 14 + README.md | 3 +- TidyBuild.custom.props | 2 +- .../FileOperationsUT/FileOperationsUT.wixproj | 5 +- .../FileOperationsUT/FileOperationsUT.wxs | 20 +- UnitTests/FileOperationsUT/run-test.bat | 188 +++++++ wixlib/PanelSwWixExtension.wxs | 31 ++ wixlib/en-US.wxl | 2 + wixlib/he-IL.wxl | 2 + 23 files changed, 1201 insertions(+), 125 deletions(-) create mode 100644 PanelSwCustomActions/ReparsePoint.cpp create mode 100644 PanelSwCustomActions/ReparsePoint.h create mode 100644 ProtoCaLib/reparsePointDetails.proto create mode 100644 UnitTests/FileOperationsUT/run-test.bat diff --git a/PanelSwCustomActions/CommonDeferred.cpp b/PanelSwCustomActions/CommonDeferred.cpp index b8b4f775..ca328eac 100644 --- a/PanelSwCustomActions/CommonDeferred.cpp +++ b/PanelSwCustomActions/CommonDeferred.cpp @@ -14,6 +14,7 @@ #include "XslTransform.h" #include "RestartLocalResources.h" #include "ConcatFiles.h" +#include "ReparsePoint.h" // ReceiverToExecutorFunc implementation. HRESULT ReceiverToExecutor(LPCSTR szReceiver, CDeferredActionBase** ppExecutor) @@ -91,6 +92,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/PanelSwCustomActions/FileOperations.cpp b/PanelSwCustomActions/FileOperations.cpp index cf86f131..a160fa35 100644 --- a/PanelSwCustomActions/FileOperations.cpp +++ b/PanelSwCustomActions/FileOperations.cpp @@ -1,7 +1,9 @@ #include "FileOperations.h" +#include "ReparsePoint.h" #include "../CaCommon/WixString.h" #include #include +#include #include #include #include @@ -9,6 +11,134 @@ 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; @@ -244,11 +374,11 @@ HRESULT CFileOperations::CopyPath(LPCWSTR szFrom, LPCWSTR szTo, bool bMove, bool ExitOnFailure(hr, "Failed formatting string"); // Remove trailing slashes - for (size_t i = ::wcslen(szFromNull) - 1; ((i > 1) && ((szFromNull[i] == L'\\') || (szFromNull[i] == L'/'))); --i) + for (size_t i = ::wcslen(szFrom) - 1; ((i > 1) && ((szFromNull[i] == L'\\') || (szFromNull[i] == L'/'))); --i) { szFromNull[i] = NULL; } - for (size_t i = ::wcslen(szToNull) - 1; ((i > 1) && ((szToNull[i] == L'\\') || (szToNull[i] == L'/'))); --i) + for (size_t i = ::wcslen(szTo) - 1; ((i > 1) && ((szToNull[i] == L'\\') || (szToNull[i] == L'/'))); --i) { szToNull[i] = NULL; } @@ -286,6 +416,8 @@ HRESULT CFileOperations::CopyPath(LPCWSTR szFrom, LPCWSTR szTo, bool bMove, bool LogUnformatted(LOGLEVEL::LOGMSG_STANDARD, true, L"Failed copying '%ls' to '%ls'; Ignoring error (%i)", szFromNull, szToNull, nRes); ExitFunction1(hr = S_FALSE); } + hr = E_FAIL; + ExitOnFailure(hr, "Failed copying file '%ls' to '%ls'. Result %i, Aborted=%i", szFromNull, szToNull, nRes, opInfo.fAnyOperationsAborted); } ExitOnWin32Error(nRes, hr, "Failed copying file '%ls' to '%ls'", szFromNull, szToNull); ExitOnNull((!opInfo.fAnyOperationsAborted), hr, E_FAIL, "Failed copying file (operation aborted)"); @@ -302,14 +434,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) { @@ -348,18 +487,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); } @@ -379,6 +521,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); @@ -521,94 +664,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"); - } while (::FindNextFile(hFind, &FindFileData)); - ExitOnNullWithLastError((::GetLastError() == ERROR_NO_MORE_FILES), hr, "Failed searching files in '%ls'", szFullFolder); + ::PathRemoveBackslash(rootFileEntry.szPath); - ::FindClose(hFind); - hFind = INVALID_HANDLE_VALUE; + hr = ListFileEntries(&rootFileEntry, L"*", true); + ExitOnFailure(hr, "Failed to enumerate file entries under '%ls'", szFolder); + + 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) + { + 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) { - WcaLog(LOGLEVEL::LOGMSG_VERBOSE, "Folder '%ls' is a symbolic link or a mount point, so not enumerating its files", szFolder); + 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); @@ -626,7 +797,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 { @@ -635,22 +806,35 @@ 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; + + if ((pRootFolder->cSubEntries & 0x10) == 0) + { + void* pNewArray = pRootFolder->pSubEntries ? + ::HeapReAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, pRootFolder->pSubEntries, (0x10 + pRootFolder->cSubEntries) * sizeof(CFileOperations::FILE_ENTRY)) + : ::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, 0x10 * sizeof(CFileOperations::FILE_ENTRY)); + ExitOnNull(pNewArray, hr, E_OUTOFMEMORY, "Failed reallocating array"); - hr = StrAllocFormatted(&szCurrFile, L"%ls%ls", szFullFolder, FindFileData.cFileName); + pRootFolder->pSubEntries = (CFileOperations::FILE_ENTRY*)pNewArray; + } + + ++pRootFolder->cSubEntries; + pNewEntry = &pRootFolder->pSubEntries[pRootFolder->cSubEntries - 1]; + pNewEntry->dwAttributes = FindFileData.dwFileAttributes; + + 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); } } @@ -673,21 +857,32 @@ 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; + + if ((pRootFolder->cSubEntries & 0x10) == 0) + { + void* pNewArray = pRootFolder->pSubEntries ? + ::HeapReAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, pRootFolder->pSubEntries, (0x10 + pRootFolder->cSubEntries) * sizeof(CFileOperations::FILE_ENTRY)) + : ::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, 0x10 * sizeof(CFileOperations::FILE_ENTRY)); + ExitOnNull(pNewArray, hr, E_OUTOFMEMORY, "Failed reallocating array"); - hr = StrAllocFormatted(&szCurrFile, L"%ls%ls", szFullFolder, FindFileData.cFileName); - ExitOnFailure(hr, "Failed allocating string"); + pRootFolder->pSubEntries = (CFileOperations::FILE_ENTRY*)pNewArray; + } + + ++pRootFolder->cSubEntries; + pNewEntry = &pRootFolder->pSubEntries[pRootFolder->cSubEntries - 1]; + pNewEntry->dwAttributes = FindFileData.dwFileAttributes; - 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)); @@ -695,15 +890,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 @@ -762,27 +966,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/PanelSwCustomActions/FileOperations.h b/PanelSwCustomActions/FileOperations.h index 57f51be6..6b0cee79 100644 --- a/PanelSwCustomActions/FileOperations.h +++ b/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/PanelSwCustomActions/PanelSwCustomActions.def b/PanelSwCustomActions/PanelSwCustomActions.def index ec0fa511..4c38b09c 100644 --- a/PanelSwCustomActions/PanelSwCustomActions.def +++ b/PanelSwCustomActions/PanelSwCustomActions.def @@ -58,3 +58,5 @@ EXPORTS IsWindowsVersionOrGreater ListProcessorFeatures ExecuteCommand + RemoveFolderEx + RemoveReparseDataSched diff --git a/PanelSwCustomActions/PanelSwCustomActions.vcxproj b/PanelSwCustomActions/PanelSwCustomActions.vcxproj index e7916ae8..775ace81 100755 --- a/PanelSwCustomActions/PanelSwCustomActions.vcxproj +++ b/PanelSwCustomActions/PanelSwCustomActions.vcxproj @@ -104,6 +104,7 @@ + @@ -129,6 +130,7 @@ + @@ -222,4 +224,4 @@ - \ No newline at end of file + diff --git a/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters b/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters index 736904dc..f8709d70 100755 --- a/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters +++ b/PanelSwCustomActions/PanelSwCustomActions.vcxproj.filters @@ -269,6 +269,9 @@ Header Files + + Header Files + Header Files @@ -287,4 +290,4 @@ - \ No newline at end of file + diff --git a/PanelSwCustomActions/ReparsePoint.cpp b/PanelSwCustomActions/ReparsePoint.cpp new file mode 100644 index 00000000..0f5a4646 --- /dev/null +++ b/PanelSwCustomActions/ReparsePoint.cpp @@ -0,0 +1,461 @@ +#include "FileOperations.h" +#include "ReparsePoint.h" +#include "reparsePointDetails.pb.h" +#include "../CaCommon/WixString.h" +#include +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; + +#define REPARSE_DATA_BUFFER_HEADER_SIZE FIELD_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."); + + 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: + if (hFind && (hFind != INVALID_HANDLE_VALUE)) + { + ::FindClose(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: + if (hFile && (hFile != INVALID_HANDLE_VALUE)) + { + ::CloseHandle(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: + if (hFile && (hFile != INVALID_HANDLE_VALUE)) + { + ::CloseHandle(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: + if (hFile && (hFile != INVALID_HANDLE_VALUE)) + { + ::CloseHandle(hFile); + } + + return hr; +} diff --git a/PanelSwCustomActions/ReparsePoint.h b/PanelSwCustomActions/ReparsePoint.h new file mode 100644 index 00000000..5e1a86f7 --- /dev/null +++ b/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/PanelSwCustomActions/SqlScript.cpp b/PanelSwCustomActions/SqlScript.cpp index d83c04f9..08dbddb2 100644 --- a/PanelSwCustomActions/SqlScript.cpp +++ b/PanelSwCustomActions/SqlScript.cpp @@ -221,9 +221,10 @@ static HRESULT ReadBinary(LPCWSTR szBinaryKey, LPCWSTR szQueryId, CWixString* ps // Ensure null-termination. scoped for local use of pbData1 { + BYTE* pbData1 = (LPBYTE)::HeapReAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, pbData, cbData + 2); + ExitOnNull(pbData1, hr, E_OUTOFMEMORY, "Failed reallocating memory"); + cbData += 2; - BYTE* pbData1 = (LPBYTE)MemReAlloc(pbData, cbData, TRUE); - ExitOnNull(pbData1, hr, E_FAIL, "Failed reallocating memory"); pbData = pbData1; } diff --git a/PanelSwWixExtension/Data/tables.xml b/PanelSwWixExtension/Data/tables.xml index 181839ff..27ec1505 100644 --- a/PanelSwWixExtension/Data/tables.xml +++ b/PanelSwWixExtension/Data/tables.xml @@ -434,12 +434,19 @@ - + - + + + + + + + + diff --git a/PanelSwWixExtension/PanelSwWixCompiler.cs b/PanelSwWixExtension/PanelSwWixCompiler.cs index 0cf69e89..de397703 100644 --- a/PanelSwWixExtension/PanelSwWixCompiler.cs +++ b/PanelSwWixExtension/PanelSwWixCompiler.cs @@ -261,6 +261,10 @@ public override void ParseElement(SourceLineNumberCollection sourceLineNumbers, ParseXslTransform(element, componentId, null); break; + case "RemoveFolderEx": + ParseRemoveFolderEx(element, componentId); + break; + default: Core.UnexpectedElement(parentElement, element); break; @@ -344,6 +348,64 @@ public override void ParseElement(SourceLineNumberCollection sourceLineNumbers, } } + enum RemoveFolderExInstallMode + { + Install = 1, + Uninstall = 2, + Both = 3, + } + + private void ParseRemoveFolderEx(XmlElement element, string componentId) + { + SourceLineNumberCollection sourceLineNumbers = Preprocessor.GetSourceLineNumbers(element); + string property = null; + RemoveFolderExInstallMode on = RemoveFolderExInstallMode.Uninstall; + + foreach (XmlAttribute attrib in element.Attributes) + { + if ((0 != attrib.NamespaceURI.Length) && (attrib.NamespaceURI != schema.TargetNamespace)) + { + continue; + } + + switch (attrib.LocalName) + { + case "Property": + property = Core.GetAttributeIdentifierValue(sourceLineNumbers, attrib); + break; + case "On": + string onName = Core.GetAttributeValue(sourceLineNumbers, attrib); + if (!Enum.TryParse(onName, true, out on)) + { + Core.OnMessage(WixErrors.IllegalAttributeValueWithLegalList(sourceLineNumbers, element.LocalName, attrib.LocalName, onName, $"{nameof(RemoveFolderExInstallMode.Install)}, {nameof(RemoveFolderExInstallMode.Uninstall)}, {nameof(RemoveFolderExInstallMode.Both)}")); + } + break; + default: + Core.UnexpectedAttribute(sourceLineNumbers, attrib); + break; + } + } + + if (string.IsNullOrEmpty(property)) + { + Core.OnMessage(WixErrors.ExpectedAttribute(sourceLineNumbers, element.LocalName, "Property")); + } + + if (Core.EncounteredError) + { + return; + } + + Core.CreateWixSimpleReferenceRow(sourceLineNumbers, "CustomAction", "PSW_RemoveFolderEx"); + Core.EnsureTable(sourceLineNumbers, "RemoveFile"); + + Row row = Core.CreateRow(sourceLineNumbers, "PSW_RemoveFolderEx"); + row[0] = $"rmf{Guid.NewGuid().ToString("N")}"; + row[1] = componentId; + row[2] = property; + row[3] = (int)on; + } + private void ParseDuplicateFolderElement(XmlElement element) { SourceLineNumberCollection sourceLineNumbers = Preprocessor.GetSourceLineNumbers(element); diff --git a/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd b/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd index aa2e6ab9..61f6a306 100644 --- a/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd +++ b/PanelSwWixExtension/Xsd/PanelSwWixExtension.xsd @@ -230,6 +230,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/ProtoCaLib/ProtoCaLib.vcxproj b/ProtoCaLib/ProtoCaLib.vcxproj index 7dc69cf9..f09de831 100644 --- a/ProtoCaLib/ProtoCaLib.vcxproj +++ b/ProtoCaLib/ProtoCaLib.vcxproj @@ -121,6 +121,9 @@ True + + True + @@ -180,6 +183,9 @@ True + + True + @@ -191,6 +197,7 @@ + diff --git a/ProtoCaLib/ProtoCaLib.vcxproj.filters b/ProtoCaLib/ProtoCaLib.vcxproj.filters index 7a685196..a57c3925 100644 --- a/ProtoCaLib/ProtoCaLib.vcxproj.filters +++ b/ProtoCaLib/ProtoCaLib.vcxproj.filters @@ -76,6 +76,9 @@ Source Files + + Source Files + @@ -135,6 +138,9 @@ Header Files + + Header Files + @@ -195,10 +201,13 @@ Protobuf + + Protobuf + Resource Files - \ No newline at end of file + diff --git a/ProtoCaLib/reparsePointDetails.proto b/ProtoCaLib/reparsePointDetails.proto new file mode 100644 index 00000000..a0784472 --- /dev/null +++ b/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/README.md b/README.md index d4aea84d..6b68148f 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,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 @@ -209,5 +210,5 @@ After building a unit test project, you'll need to shutdown Visual Studio before This is due to the unfortunate habit of Visual Studio to hold the extension file in use. You may find it convenient to build unit test projects from a command prompt to workaround this limitation ~~~~~~~~~~~~ -MSBuild UnitTests\IsWindowsVersionOrGreaterUT\IsWindowsVersionOrGreaterUT.wixproj /p:Configuration=Release /p:Platform=x86 /t:Rebuild "/p:SolutionDir=%CD%\\" +MSBuild UnitTests\FileOperationsUT\FileOperationsUT.wixproj /p:Configuration=Release /p:Platform=x86 /t:Rebuild "/p:SolutionDir=%CD%\\" ~~~~~~~~~~~~ diff --git a/TidyBuild.custom.props b/TidyBuild.custom.props index 16968761..1998a6b4 100644 --- a/TidyBuild.custom.props +++ b/TidyBuild.custom.props @@ -2,7 +2,7 @@ - 3.12.1 + 3.13.0 $(FullVersion).$(GITHUB_RUN_NUMBER) PanelSwWixExtension Panel::Software diff --git a/UnitTests/FileOperationsUT/FileOperationsUT.wixproj b/UnitTests/FileOperationsUT/FileOperationsUT.wixproj index 8dd93ce7..1f0eaa13 100644 --- a/UnitTests/FileOperationsUT/FileOperationsUT.wixproj +++ b/UnitTests/FileOperationsUT/FileOperationsUT.wixproj @@ -20,6 +20,9 @@ + + + $(WixToolPath)\WixUtilExtension.dll @@ -52,4 +55,4 @@ --> - \ No newline at end of file + diff --git a/UnitTests/FileOperationsUT/FileOperationsUT.wxs b/UnitTests/FileOperationsUT/FileOperationsUT.wxs index e98c2389..722ec1b0 100644 --- a/UnitTests/FileOperationsUT/FileOperationsUT.wxs +++ b/UnitTests/FileOperationsUT/FileOperationsUT.wxs @@ -36,6 +36,24 @@ + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/UnitTests/FileOperationsUT/run-test.bat b/UnitTests/FileOperationsUT/run-test.bat new file mode 100644 index 00000000..c887afc0 --- /dev/null +++ b/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/wixlib/PanelSwWixExtension.wxs b/wixlib/PanelSwWixExtension.wxs index b84a9277..3098e447 100755 --- a/wixlib/PanelSwWixExtension.wxs +++ b/wixlib/PanelSwWixExtension.wxs @@ -947,4 +947,35 @@ + + + + + + + + + + + + + !(loc.PSW_RemoveFolderEx)" + + + + + + + + + + + + + + !(loc.RemoveReparseData)" + !(loc.RemoveReparseData)" + !(loc.RemoveReparseData)" + + diff --git a/wixlib/en-US.wxl b/wixlib/en-US.wxl index 42627d1f..35960fe7 100755 --- a/wixlib/en-US.wxl +++ b/wixlib/en-US.wxl @@ -80,4 +80,6 @@ + + diff --git a/wixlib/he-IL.wxl b/wixlib/he-IL.wxl index cd349585..8143f40f 100644 --- a/wixlib/he-IL.wxl +++ b/wixlib/he-IL.wxl @@ -82,4 +82,6 @@ + +