InstalledFilesCorrelation.cpp (13083B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "winget/InstalledFilesCorrelation.h" 5 #include <winget/FolderFileWatcher.h> 6 #include <winget/Filesystem.h> 7 8 using namespace AppInstaller::Manifest; 9 using namespace AppInstaller::Repository; 10 using namespace AppInstaller::Utility; 11 12 namespace AppInstaller::Repository::Correlation 13 { 14 namespace 15 { 16 constexpr std::string_view s_ShellLinkFileExtension = ".lnk"sv; 17 const std::vector<std::pair<std::filesystem::path, std::string>> s_CandidateInstallLocationRoots = 18 { 19 { Filesystem::GetKnownFolderPath(FOLDERID_LocalAppData), "%LOCALAPPDATA%" }, 20 { Filesystem::GetKnownFolderPath(FOLDERID_ProgramFiles), "%PROGRAMFILES%" }, 21 { Filesystem::GetKnownFolderPath(FOLDERID_ProgramFilesX86), "%PROGRAMFILES(X86)%" }, 22 }; 23 24 // Contains shell link info 25 struct ShellLinkFileInfo 26 { 27 std::filesystem::path Path; 28 std::string Args; 29 std::string DisplayName; 30 }; 31 32 std::optional<ShellLinkFileInfo> ParseShellLinkFile(const std::filesystem::path& linkFile) 33 { 34 try 35 { 36 AICLI_LOG(Repo, Info, << "Parsing link file at " << linkFile); 37 38 ShellLinkFileInfo result; 39 40 Microsoft::WRL::ComPtr<IShellLink> shellLink; 41 THROW_IF_FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink))); 42 Microsoft::WRL::ComPtr<IPersistFile> persistFile; 43 THROW_IF_FAILED(shellLink.As(&persistFile)); 44 THROW_IF_FAILED(persistFile->Load(linkFile.wstring().c_str(), STGM_READ)); 45 THROW_IF_FAILED(shellLink->Resolve(nullptr, SLR_NO_UI | SLR_NOUPDATE | SLR_NOSEARCH | SLR_NOTRACK | SLR_NOLINKINFO)); 46 47 { 48 // Parse Path from shell link 49 std::wstring buffer; 50 buffer.resize(MAX_PATH); 51 HRESULT hr = S_OK; 52 for (int retry = 0; retry < 5; retry++) 53 { 54 hr = shellLink->GetPath( 55 &buffer[0], 56 static_cast<int>(buffer.size()), 57 nullptr, 58 0 59 ); 60 61 if (SUCCEEDED(hr)) 62 { 63 buffer.erase(std::find(buffer.begin(), buffer.end(), L'\0'), buffer.end()); 64 result.Path = buffer; 65 break; 66 } 67 else if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) 68 { 69 buffer.resize(buffer.size() * 2); 70 } 71 else 72 { 73 THROW_IF_FAILED(hr); 74 } 75 } 76 } 77 78 { 79 // Parse arguments from shell link 80 std::wstring buffer; 81 buffer.resize(MAX_PATH); 82 HRESULT hr = S_OK; 83 for (int retry = 0; retry < 5; retry++) 84 { 85 hr = shellLink->GetArguments( 86 &buffer[0], 87 static_cast<int>(buffer.size())); 88 89 if (SUCCEEDED(hr)) 90 { 91 buffer.erase(std::find(buffer.begin(), buffer.end(), L'\0'), buffer.end()); 92 result.Args = Utility::ConvertToUTF8(buffer); 93 break; 94 } 95 else if (hr == HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER)) 96 { 97 buffer.resize(buffer.size() * 2); 98 } 99 else 100 { 101 THROW_IF_FAILED(hr); 102 } 103 } 104 } 105 106 // Use shell link file name (minus extension) as display name. 107 result.DisplayName = linkFile.stem().u8string(); 108 109 AICLI_LOG(Repo, Info, << "Link file parsed. Path: " << result.Path << " Args: " << result.Args << " DisplayName: " << result.DisplayName); 110 111 return result; 112 } 113 catch (...) 114 { 115 AICLI_LOG(Repo, Error, << "Failed to parse link file at " << linkFile); 116 return {}; 117 } 118 } 119 120 // Returns nullopt if path is not under base. 121 std::optional<std::filesystem::path> GetRelativePath(const std::filesystem::path& path, const std::filesystem::path& base) 122 { 123 auto canonicalPath = std::filesystem::weakly_canonical(path); 124 auto canonicalBase = std::filesystem::weakly_canonical(base); 125 126 auto relativePath = std::filesystem::relative(canonicalPath, canonicalBase); 127 if (!relativePath.empty() && *relativePath.begin() != "." && *relativePath.begin() != "..") 128 { 129 return relativePath; 130 } 131 else 132 { 133 return {}; 134 } 135 } 136 137 std::optional<std::filesystem::path> CheckOneInstallLocation(const std::filesystem::path& childFile, const std::filesystem::path& baseFolder) 138 { 139 auto relativePath = GetRelativePath(childFile, baseFolder); 140 if (relativePath) 141 { 142 // TODO: Here we assume the install location is the top directory of relative path. 143 auto installLocation = baseFolder / *relativePath->begin(); 144 if (std::filesystem::exists(installLocation) && std::filesystem::is_directory(installLocation)) 145 { 146 return installLocation; 147 } 148 } 149 150 return {}; 151 } 152 153 // If install location is not provided in arp entry, try LocalAppData folder and Program Files folders. 154 std::optional<std::filesystem::path> CheckInstallLocation(const std::filesystem::path& path) 155 { 156 for (auto const& entry : s_CandidateInstallLocationRoots) 157 { 158 auto installLocation = CheckOneInstallLocation(path, entry.first); 159 if (installLocation) 160 { 161 return installLocation; 162 } 163 } 164 165 return {}; 166 } 167 168 // TODO: basic heuristics to determine file type. 169 AppInstaller::Manifest::InstalledFileTypeEnum GetInstalledFileType(const ShellLinkFileInfo& linkInfo) 170 { 171 Manifest::InstalledFileTypeEnum result = Manifest::InstalledFileTypeEnum::Other; 172 173 if (Utility::CaseInsensitiveContainsSubstring(linkInfo.Path.u8string(), "uninstall") || 174 Utility::CaseInsensitiveContainsSubstring(linkInfo.Path.u8string(), "unins000") || 175 Utility::CaseInsensitiveContainsSubstring(linkInfo.Args, "uninstall") || 176 Utility::CaseInsensitiveContainsSubstring(linkInfo.DisplayName, "uninstall")) 177 { 178 result = Manifest::InstalledFileTypeEnum::Uninstall; 179 } 180 else if (Utility::CaseInsensitiveEquals(linkInfo.Path.extension().u8string(), ".exe")) 181 { 182 result = Manifest::InstalledFileTypeEnum::Launch; 183 } 184 185 return result; 186 } 187 188 std::string GetUnexpandedInstallLocation(const std::filesystem::path& installLocation) 189 { 190 // Try to match the candidate install location roots first. 191 std::filesystem::path resultInstallLocation = installLocation; 192 for (auto const& entry : s_CandidateInstallLocationRoots) 193 { 194 if (Filesystem::ReplaceCommonPathPrefix(resultInstallLocation, entry.first, entry.second)) 195 { 196 return resultInstallLocation.u8string(); 197 } 198 } 199 200 // Then try PathUnExpandEnvStrings OS api 201 std::wstring installLocationWString = installLocation.wstring(); 202 std::wstring buffer; 203 buffer.resize(installLocationWString.size() + 20); 204 if (PathUnExpandEnvStrings( 205 installLocationWString.c_str(), 206 &buffer[0], 207 static_cast<int>(buffer.size()))) 208 { 209 buffer.resize(buffer.find(L'\0')); 210 return Utility::ConvertToUTF8(buffer); 211 } 212 213 return resultInstallLocation.u8string(); 214 } 215 } 216 217 InstalledFilesCorrelation::InstalledFilesCorrelation() 218 { 219 m_fileWatchers.emplace_back(Filesystem::GetKnownFolderPath(FOLDERID_CommonStartMenu), std::string{ s_ShellLinkFileExtension }); 220 m_fileWatchers.emplace_back(Filesystem::GetKnownFolderPath(FOLDERID_StartMenu), std::string{ s_ShellLinkFileExtension }); 221 } 222 223 void InstalledFilesCorrelation::StartFileWatcher() 224 { 225 m_files.clear(); 226 227 for (auto& watcher : m_fileWatchers) 228 { 229 watcher.Start(); 230 } 231 } 232 233 void InstalledFilesCorrelation::StopFileWatcher() 234 { 235 for (auto& watcher : m_fileWatchers) 236 { 237 watcher.Stop(); 238 } 239 240 for (auto& watcher : m_fileWatchers) 241 { 242 FileWatcherFiles files; 243 files.Folder = watcher.FolderPath(); 244 245 for (auto const& file : watcher.Files()) 246 { 247 files.Files.emplace_back(file); 248 } 249 250 m_files.emplace_back(std::move(files)); 251 } 252 } 253 254 InstallationMetadata InstalledFilesCorrelation::CorrelateForNewlyInstalled( 255 const Manifest::Manifest&, 256 const std::string& arpInstallLocation) 257 { 258 InstallationMetadata result; 259 260 std::filesystem::path installLocation; 261 // Use arp install location if provided 262 if (!arpInstallLocation.empty()) 263 { 264 installLocation = Filesystem::GetExpandedPath(arpInstallLocation); 265 } 266 267 for (auto const& files : m_files) 268 { 269 for (auto const& file : files.Files) 270 { 271 // TODO: we only watch shell link files at the moment. 272 auto linkInfo = ParseShellLinkFile(files.Folder / file); 273 if (linkInfo) 274 { 275 auto installedFileType = GetInstalledFileType(linkInfo.value()); 276 277 // Collect installed files metadata if exist 278 if (std::filesystem::exists(linkInfo->Path) && std::filesystem::is_regular_file(linkInfo->Path)) 279 { 280 if (installLocation.empty()) 281 { 282 // TODO: In most cases, installed files are under same folder, so use the first file to determine install location at the moment. 283 auto location = CheckInstallLocation(linkInfo->Path); 284 if (!location) 285 { 286 continue; 287 } 288 289 installLocation = location.value(); 290 } 291 292 auto relativePath = GetRelativePath(linkInfo->Path, installLocation); 293 if (relativePath) 294 { 295 AppInstaller::Manifest::InstalledFile fileEntry; 296 fileEntry.RelativeFilePath = relativePath->string(); 297 std::ifstream in{ linkInfo->Path, std::ifstream::binary }; 298 fileEntry.FileSha256 = Utility::SHA256::ComputeHash(in); 299 fileEntry.InvocationParameter = linkInfo->Args; 300 fileEntry.DisplayName = linkInfo->DisplayName; 301 fileEntry.FileType = installedFileType; 302 result.InstalledFiles.Files.emplace_back(std::move(fileEntry)); 303 } 304 } 305 306 // Collect short cut paths 307 InstalledStartupLinkFile linkFile; 308 linkFile.RelativeFilePath = file.u8string(); 309 linkFile.FileType = installedFileType; 310 result.StartupLinkFiles.emplace_back(linkFile); 311 } 312 } 313 } 314 315 if (!installLocation.empty()) 316 { 317 result.InstalledFiles.DefaultInstallLocation = GetUnexpandedInstallLocation(installLocation); 318 } 319 320 return result; 321 } 322 }