winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }