winget-cli

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

InstallerMetadataCollectionContext.cpp (67517B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "winget/InstallerMetadataCollectionContext.h"
      5 
      6 #include <AppInstallerDownloader.h>
      7 #include <AppInstallerErrors.h>
      8 #include <AppInstallerLogging.h>
      9 #include <AppInstallerFileLogger.h>
     10 #include <winget/TraceLogger.h>
     11 #include <AppInstallerStrings.h>
     12 #include <winget/JsonUtil.h>
     13 #include <winget/ManifestJSONParser.h>
     14 
     15 using namespace AppInstaller::Utility;
     16 
     17 namespace AppInstaller::Repository::Metadata
     18 {
     19     namespace
     20     {
     21         struct ProductMetadataFields_1_N
     22         {
     23             ProductMetadataFields_1_N(const Version& version)
     24             {
     25                 if (::AppInstaller::Utility::Version{ "1.1" } <= version)
     26                 {
     27                     SchemaVersion = L"1.1";
     28                     Scope = L"scope";
     29                 }
     30 
     31                 if (::AppInstaller::Utility::Version{ "1.2" } <= version)
     32                 {
     33                     SchemaVersion = L"1.2";
     34                     InstalledFiles = L"installedFiles";
     35                     DefaultInstallLocation = L"DefaultInstallLocation";
     36                     InstallationMetadataFiles = L"Files";
     37                     InstalledFileRelativeFilePath = L"RelativeFilePath";
     38                     InstalledFileSha256 = L"FileSha256";
     39                     InstalledFileType = L"FileType";
     40                     InstalledFileInvocationParameter = L"InvocationParameter";
     41                     InstalledFileDisplayName = L"DisplayName";
     42                     InstalledStartupLinks = L"startupLinks";
     43                     InstalledStartupLinkPath = L"RelativeFilePath";
     44                     InstalledStartupLinkType = L"FileType";
     45                     Icons = L"icons";
     46                     IconContent = L"IconContent";
     47                     IconSha256 = L"IconSha256";
     48                     IconFileType = L"IconFileType";
     49                     IconResolution = L"IconResolution";
     50                     IconTheme = L"IconTheme";
     51                 }
     52             }
     53 
     54             utility::string_t SchemaVersion = L"1.0";
     55 
     56             // 1.0
     57             utility::string_t ProductVersionMin = L"productVersionMin";
     58             utility::string_t ProductVersionMax = L"productVersionMax";
     59             utility::string_t Metadata = L"metadata";
     60             utility::string_t InstallerHash = L"installerHash";
     61             utility::string_t SubmissionIdentifier = L"submissionIdentifier";
     62             utility::string_t Version = L"version";
     63             utility::string_t AppsAndFeaturesEntries = L"AppsAndFeaturesEntries";
     64             utility::string_t Historical = L"historical";
     65 
     66             // AppsAndFeaturesEntry fields.
     67             utility::string_t DisplayName = L"DisplayName";
     68             utility::string_t Publisher = L"Publisher";
     69             utility::string_t DisplayVersion = L"DisplayVersion";
     70             utility::string_t ProductCode = L"ProductCode";
     71             utility::string_t UpgradeCode = L"UpgradeCode";
     72             utility::string_t InstallerType = L"InstallerType";
     73 
     74             utility::string_t VersionMin = L"versionMin";
     75             utility::string_t VersionMax = L"versionMax";
     76             utility::string_t Names = L"names";
     77             utility::string_t Publishers = L"publishers";
     78             utility::string_t ProductCodes = L"productCodes";
     79             utility::string_t UpgradeCodes = L"upgradeCodes";
     80 
     81             // 1.1
     82             utility::string_t Scope;
     83 
     84             // 1.2
     85 
     86             // Installed files
     87             utility::string_t InstalledFiles;
     88             utility::string_t DefaultInstallLocation;
     89             utility::string_t InstallationMetadataFiles;
     90             utility::string_t InstalledFileRelativeFilePath;
     91             utility::string_t InstalledFileSha256;
     92             utility::string_t InstalledFileType;
     93             utility::string_t InstalledFileInvocationParameter;
     94             utility::string_t InstalledFileDisplayName;
     95             // Startup links
     96             utility::string_t InstalledStartupLinks;
     97             utility::string_t InstalledStartupLinkPath;
     98             utility::string_t InstalledStartupLinkType;
     99             // Icons
    100             utility::string_t Icons;
    101             utility::string_t IconContent;
    102             utility::string_t IconSha256;
    103             utility::string_t IconFileType;
    104             utility::string_t IconResolution;
    105             utility::string_t IconTheme;
    106         };
    107 
    108         struct OutputFields_1_0
    109         {
    110             utility::string_t Version = L"version";
    111             utility::string_t SubmissionData = L"submissionData";
    112             utility::string_t InstallerHash = L"installerHash";
    113             utility::string_t Status = L"status";
    114             utility::string_t Metadata = L"metadata";
    115             utility::string_t Diagnostics = L"diagnostics";
    116         };
    117         
    118         struct DiagnosticFields
    119         {
    120             // Error case
    121             utility::string_t ErrorHR = L"errorHR";
    122             utility::string_t ErrorText = L"errorText";
    123 
    124             // Non-error case
    125             utility::string_t Reason = L"reason";
    126             utility::string_t ChangedEntryCount = L"changedEntryCount";
    127             utility::string_t MatchedEntryCount = L"matchedEntryCount";
    128             utility::string_t IntersectionCount = L"intersectionCount";
    129             utility::string_t CorrelationMeasures = L"correlationMeasures";
    130             utility::string_t Value = L"value";
    131             utility::string_t Name = L"name";
    132             utility::string_t Publisher = L"publisher";
    133         };
    134 
    135         std::string GetRequiredString(const web::json::value& value, const utility::string_t& field)
    136         {
    137             auto optString = AppInstaller::JSON::GetRawStringValueFromJsonNode(value, field);
    138             if (!optString)
    139             {
    140                 AICLI_LOG(Repo, Error, << "Required field '" << Utility::ConvertToUTF8(field) << "' was not present");
    141                 THROW_HR(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE);
    142             }
    143             return std::move(optString).value();
    144         }
    145 
    146         void AddFieldIfNotEmpty(web::json::value& value, const utility::string_t& field, std::string_view string)
    147         {
    148             if (!string.empty())
    149             {
    150                 value[field] = AppInstaller::JSON::GetStringValue(string);
    151             }
    152         }
    153 
    154         web::json::value CreateStringArray(const std::set<std::string>& values)
    155         {
    156             web::json::value result;
    157             size_t index = 0;
    158 
    159             for (const std::string& value : values)
    160             {
    161                 result[index++] = AppInstaller::JSON::GetStringValue(value);
    162             }
    163 
    164             return result;
    165         }
    166 
    167         bool AddIfNotPresentAndNotEmpty(std::set<std::string>& strings, const std::set<std::string>& filter, const std::string& string)
    168         {
    169             if (string.empty() || filter.find(string) != filter.end())
    170             {
    171                 return false;
    172             }
    173 
    174             strings.emplace(string);
    175             return true;
    176         }
    177 
    178         bool AddIfNotPresentAndNotEmpty(std::set<std::string>& strings, const std::string& string)
    179         {
    180             return AddIfNotPresentAndNotEmpty(strings, strings, string);
    181         }
    182 
    183         void AddIfNotPresent(std::set<std::string>& strings, std::set<std::string>& filter, const std::set<std::string>& inputs)
    184         {
    185             for (const std::string& input : inputs)
    186             {
    187                 if (AddIfNotPresentAndNotEmpty(strings, filter, input))
    188                 {
    189                     filter.emplace(input);
    190                 }
    191             }
    192         }
    193 
    194         void FilterAndAddToEntries(Manifest::AppsAndFeaturesEntry&& newEntry, std::vector<Manifest::AppsAndFeaturesEntry>& entries)
    195         {
    196             // Erase all duplicated data from the new entry
    197             for (const auto& entry : entries)
    198             {
    199 #define WINGET_ERASE_IF_SAME(_value_) if (entry._value_ == newEntry._value_) { newEntry._value_.clear(); }
    200                 WINGET_ERASE_IF_SAME(DisplayName);
    201                 WINGET_ERASE_IF_SAME(DisplayVersion);
    202                 WINGET_ERASE_IF_SAME(ProductCode);
    203                 WINGET_ERASE_IF_SAME(Publisher);
    204                 WINGET_ERASE_IF_SAME(UpgradeCode);
    205 #undef WINGET_ERASE_IF_SAME
    206 
    207                 if (entry.InstallerType == newEntry.InstallerType)
    208                 {
    209                     newEntry.InstallerType = Manifest::InstallerTypeEnum::Unknown;
    210                 }
    211             }
    212 
    213             // If anything remains, add it
    214             if (!newEntry.DisplayName.empty() || !newEntry.DisplayVersion.empty() || !newEntry.ProductCode.empty() ||
    215                 !newEntry.Publisher.empty() || !newEntry.UpgradeCode.empty() || newEntry.InstallerType != Manifest::InstallerTypeEnum::Unknown)
    216             {
    217                 entries.emplace_back(std::move(newEntry));
    218             }
    219         }
    220 
    221         std::optional<std::string> GetStringFromFutureSchema(const web::json::value& value, const utility::string_t& field)
    222         {
    223             if (field.empty())
    224             {
    225                 return {};
    226             }
    227 
    228             return AppInstaller::JSON::GetRawStringValueFromJsonNode(value, field);
    229         }
    230 
    231         void SetStringFromFutureSchema(web::json::value& json, const utility::string_t& field, std::string_view value)
    232         {
    233             if (!field.empty())
    234             {
    235                 json[field] = AppInstaller::JSON::GetStringValue(value);
    236             }
    237         }
    238 
    239         // For installed files merging, we remove conflicting entries, like scope. Indicating we are not certain some files will always be installed.
    240         void MergeInstalledFilesMetadata(Manifest::InstallationMetadataInfo& existing, const Manifest::InstallationMetadataInfo& incoming)
    241         {
    242             if (!Utility::CaseInsensitiveEquals(existing.DefaultInstallLocation, incoming.DefaultInstallLocation))
    243             {
    244                 existing.Clear();
    245                 return;
    246             }
    247 
    248             auto existingItr = existing.Files.begin();
    249             while (existingItr != existing.Files.end())
    250             {
    251                 auto itr = std::find_if(incoming.Files.begin(), incoming.Files.end(), [&](const Manifest::InstalledFile& entry)
    252                     {
    253                         return Utility::CaseInsensitiveEquals(existingItr->RelativeFilePath, entry.RelativeFilePath);
    254                     });
    255 
    256                 if (itr == incoming.Files.end())
    257                 {
    258                     existingItr = existing.Files.erase(existingItr);
    259                 }
    260                 else
    261                 {
    262                     if (existingItr->InvocationParameter != itr->InvocationParameter)
    263                     {
    264                         existingItr->InvocationParameter.clear();
    265                     }
    266                     if (!Utility::CaseInsensitiveEquals(existingItr->DisplayName, itr->DisplayName))
    267                     {
    268                         existingItr->DisplayName.clear();
    269                     }
    270                     if (!Utility::SHA256::AreEqual(existingItr->FileSha256, itr->FileSha256))
    271                     {
    272                         existingItr->FileSha256.clear();
    273                     }
    274                     if (existingItr->FileType != itr->FileType)
    275                     {
    276                         existingItr->FileType = Manifest::InstalledFileTypeEnum::Unknown;
    277                     }
    278 
    279                     ++existingItr;
    280                 }
    281             }
    282         }
    283 
    284         // For startup link files merging, we add non duplicate entries, like ProductCodes. Indicating possible startup links an installer could potentially add.
    285         void MergeStartupLinkFilesMetadata(std::vector<Correlation::InstalledStartupLinkFile>& existing, const std::vector<Correlation::InstalledStartupLinkFile>& incoming)
    286         {
    287             for (auto const& incomingEntry : incoming)
    288             {
    289                 auto itr = std::find_if(existing.begin(), existing.end(), [&](const Correlation::InstalledStartupLinkFile& entry)
    290                     {
    291                         return Utility::CaseInsensitiveEquals(incomingEntry.RelativeFilePath, entry.RelativeFilePath);
    292                     });
    293 
    294                 if (itr == existing.end())
    295                 {
    296                     existing.emplace_back(incomingEntry);
    297                 }
    298                 else if (itr->FileType != incomingEntry.FileType)
    299                 {
    300                     // Set conflicting file type to Unknown.
    301                     itr->FileType = AppInstaller::Manifest::InstalledFileTypeEnum::Unknown;
    302                 }
    303             }
    304         }
    305 
    306         // TODO: This method could be moved to rest response parser and reused when winget supports launch
    307         // scenarios (i.e. when startup links info are exposed in winget manifest).
    308         std::optional<std::vector<Correlation::InstalledStartupLinkFile>> DeserializeInstalledStartupLinks(
    309             const web::json::value& startupLinkFiles,
    310             const ProductMetadataFields_1_N& fields)
    311         {
    312             if (startupLinkFiles.is_null() || !startupLinkFiles.is_array())
    313             {
    314                 return {};
    315             }
    316 
    317             std::vector<Correlation::InstalledStartupLinkFile> startupLinks;
    318             for (auto const& startupLink : startupLinkFiles.as_array())
    319             {
    320                 Correlation::InstalledStartupLinkFile fileEntry;
    321 
    322                 std::optional<std::string> relativeFilePath = AppInstaller::JSON::GetRawStringValueFromJsonNode(startupLink, fields.InstalledStartupLinkPath);
    323                 if (!AppInstaller::JSON::IsValidNonEmptyStringValue(relativeFilePath))
    324                 {
    325                     AICLI_LOG(Repo, Error, << "Missing RelativeFilePath in Installed Startup Link Files.");
    326                     return {};
    327                 }
    328 
    329                 fileEntry.RelativeFilePath = std::move(*relativeFilePath);
    330 
    331                 std::optional<std::string> fileType = AppInstaller::JSON::GetRawStringValueFromJsonNode(startupLink, fields.InstalledStartupLinkType);
    332                 if (AppInstaller::JSON::IsValidNonEmptyStringValue(fileType))
    333                 {
    334                     fileEntry.FileType = Manifest::ConvertToInstalledFileTypeEnum(*fileType);
    335                 }
    336 
    337                 startupLinks.emplace_back(std::move(fileEntry));
    338             }
    339 
    340             return startupLinks;
    341         }
    342 
    343         std::vector<ExtractedIconInfo> DeserializeExtractedIcons(
    344             const web::json::value& icons,
    345             const ProductMetadataFields_1_N& fields)
    346         {
    347             if (icons.is_null() || !icons.is_array())
    348             {
    349                 return {};
    350             }
    351 
    352             std::vector<ExtractedIconInfo> result;
    353             for (auto const& iconInfo : icons.as_array())
    354             {
    355                 ExtractedIconInfo iconInfoEntry;
    356 
    357                 auto content = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconContent);
    358                 if (!AppInstaller::JSON::IsValidNonEmptyStringValue(content))
    359                 {
    360                     AICLI_LOG(Repo, Error, << "Missing IconContent in Extracted Icons.");
    361                     return {};
    362                 }
    363 
    364                 iconInfoEntry.IconContent = AppInstaller::JSON::Base64Decode(*content);
    365 
    366                 std::optional<std::string> sha256 = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconSha256);
    367                 if (AppInstaller::JSON::IsValidNonEmptyStringValue(sha256))
    368                 {
    369                     iconInfoEntry.IconSha256 = Utility::SHA256::ConvertToBytes(*sha256);
    370                 }
    371 
    372                 std::optional<std::string> fileType = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconFileType);
    373                 if (AppInstaller::JSON::IsValidNonEmptyStringValue(fileType))
    374                 {
    375                     iconInfoEntry.IconFileType = Manifest::ConvertToIconFileTypeEnum(*fileType);
    376                 }
    377 
    378                 std::optional<std::string> theme = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconTheme);
    379                 if (AppInstaller::JSON::IsValidNonEmptyStringValue(theme))
    380                 {
    381                     iconInfoEntry.IconTheme = Manifest::ConvertToIconThemeEnum(*theme);
    382                 }
    383 
    384                 std::optional<std::string> resolution = AppInstaller::JSON::GetRawStringValueFromJsonNode(iconInfo, fields.IconResolution);
    385                 if (AppInstaller::JSON::IsValidNonEmptyStringValue(resolution))
    386                 {
    387                     iconInfoEntry.IconResolution = Manifest::ConvertToIconResolutionEnum(*resolution);
    388                 }
    389 
    390                 result.emplace_back(std::move(iconInfoEntry));
    391             }
    392 
    393             return result;
    394         }
    395     }
    396 
    397     void ProductMetadata::Clear()
    398     {
    399         SchemaVersion = {};
    400         ProductVersionMin = {};
    401         ProductVersionMax = {};
    402         InstallerMetadataMap.clear();
    403         HistoricalMetadataList.clear();
    404     }
    405 
    406     void ProductMetadata::FromJson(const web::json::value& json)
    407     {
    408         Clear();
    409 
    410         utility::string_t versionFieldName = L"version";
    411 
    412         THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, json.is_null());
    413 
    414         SchemaVersion = Version{ GetRequiredString(json, versionFieldName) };
    415         AICLI_LOG(Repo, Info, << "Parsing metadata JSON version " << SchemaVersion.ToString());
    416 
    417         if (SchemaVersion.PartAt(0).Integer == 1)
    418         {
    419             FromJson_1_N(json);
    420         }
    421         else
    422         {
    423             AICLI_LOG(Repo, Error, << "Don't know how to handle metadata version " << SchemaVersion.ToString());
    424             THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE));
    425         }
    426 
    427         // Sort the historical data with oldest last (thus b < a)
    428         std::sort(HistoricalMetadataList.begin(), HistoricalMetadataList.end(),
    429             [](const HistoricalMetadata& a, const HistoricalMetadata& b) {
    430                 return b.ProductVersionMin < a.ProductVersionMin;
    431             });
    432     }
    433 
    434     web::json::value ProductMetadata::ToJson(const Utility::Version& schemaVersion, size_t maximumSizeInBytes)
    435     {
    436         SchemaVersion = schemaVersion;
    437         AICLI_LOG(Repo, Info, << "Creating metadata JSON version " << SchemaVersion.ToString());
    438 
    439         using ToJsonFunctionPointer = web::json::value(ProductMetadata::*)();
    440         ToJsonFunctionPointer toJsonFunction = nullptr;
    441 
    442         if (SchemaVersion.PartAt(0).Integer == 1)
    443         {
    444             toJsonFunction = &ProductMetadata::ToJson_1_N;
    445         }
    446         else
    447         {
    448             AICLI_LOG(Repo, Error, << "Don't know how to handle metadata version " << SchemaVersion.ToString());
    449             THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE));
    450         }
    451 
    452         // Constrain the result based on maximum size given
    453         web::json::value result = (this->*toJsonFunction)();
    454 
    455         while (maximumSizeInBytes)
    456         {
    457             // Determine current size
    458             std::ostringstream temp;
    459             result.serialize(temp);
    460 
    461             std::string tempStr = temp.str();
    462             if (tempStr.length() > maximumSizeInBytes)
    463             {
    464                 if (!DropOldestHistoricalData())
    465                 {
    466                     AICLI_LOG(Repo, Error, << "Could not remove any more historical data to get under " << maximumSizeInBytes << " bytes");
    467                     AICLI_LOG(Repo, Info, << "  Smallest size was " << tempStr.length() << " bytes with value:\n" << tempStr);
    468                     THROW_HR(HRESULT_FROM_WIN32(ERROR_FILE_TOO_LARGE));
    469                 }
    470                 result = (this->*toJsonFunction)();
    471             }
    472             else
    473             {
    474                 break;
    475             }
    476         }
    477 
    478         return result;
    479     }
    480 
    481     void ProductMetadata::CopyFrom(const ProductMetadata& source, std::string_view submissionIdentifier)
    482     {
    483         // If the source has no installer metadata, consider it empty
    484         if (source.InstallerMetadataMap.empty())
    485         {
    486             return;
    487         }
    488 
    489         // With the same submission, just copy over all of the data
    490         if (source.InstallerMetadataMap.begin()->second.SubmissionIdentifier == submissionIdentifier)
    491         {
    492             *this = source;
    493             return;
    494         }
    495 
    496         // This is a new submission, so we must move all of the data to historical and update the older historical data
    497         // First, create a new historical entry for the current metadata
    498         HistoricalMetadata currentHistory;
    499 
    500         currentHistory.ProductVersionMin = source.ProductVersionMin;
    501         currentHistory.ProductVersionMax = source.ProductVersionMax;
    502 
    503         for (const auto& metadataItem : source.InstallerMetadataMap)
    504         {
    505             for (const auto& entry : metadataItem.second.AppsAndFeaturesEntries)
    506             {
    507                 AddIfNotPresentAndNotEmpty(currentHistory.Names, entry.DisplayName);
    508                 AddIfNotPresentAndNotEmpty(currentHistory.Publishers, entry.Publisher);
    509                 AddIfNotPresentAndNotEmpty(currentHistory.ProductCodes, entry.ProductCode);
    510                 AddIfNotPresentAndNotEmpty(currentHistory.UpgradeCodes, entry.UpgradeCode);
    511             }
    512         }
    513 
    514         // Copy the data in so that we can continue using currentHistory to track all strings
    515         HistoricalMetadataList.emplace_back(currentHistory);
    516 
    517         // Now, copy over the other historical data, filtering out anything we have seen
    518         for (const auto& historical : source.HistoricalMetadataList)
    519         {
    520             HistoricalMetadata copied;
    521             copied.ProductVersionMin = historical.ProductVersionMin;
    522             copied.ProductVersionMax = historical.ProductVersionMax;
    523             AddIfNotPresent(copied.Names, currentHistory.Names, historical.Names);
    524             AddIfNotPresent(copied.Publishers, currentHistory.Publishers, historical.Publishers);
    525             AddIfNotPresent(copied.ProductCodes, currentHistory.ProductCodes, historical.ProductCodes);
    526             AddIfNotPresent(copied.UpgradeCodes, currentHistory.UpgradeCodes, historical.UpgradeCodes);
    527 
    528             if (!copied.Names.empty() || !copied.Publishers.empty() || !copied.ProductCodes.empty() || !copied.UpgradeCodes.empty())
    529             {
    530                 HistoricalMetadataList.emplace_back(std::move(copied));
    531             }
    532         }
    533     }
    534 
    535     void ProductMetadata::FromJson_1_N(const web::json::value& json)
    536     {
    537         AICLI_LOG(Repo, Info, << "Parsing metadata JSON " << SchemaVersion.ToString() << " fields");
    538 
    539         ProductMetadataFields_1_N fields{ SchemaVersion };
    540 
    541         auto productVersionMinString = AppInstaller::JSON::GetRawStringValueFromJsonNode(json, fields.ProductVersionMin);
    542         if (productVersionMinString)
    543         {
    544             ProductVersionMin = Version{ std::move(productVersionMinString).value() };
    545         }
    546 
    547         auto productVersionMaxString = AppInstaller::JSON::GetRawStringValueFromJsonNode(json, fields.ProductVersionMax);
    548         if (productVersionMaxString)
    549         {
    550             ProductVersionMax = Version{ std::move(productVersionMaxString).value() };
    551         }
    552 
    553         // The 1.0 version of metadata uses the 1.5 version of REST
    554         JSON::ManifestJSONParser parser{ Version{ "1.5" } };
    555 
    556         std::string submissionIdentifierVerification;
    557 
    558         auto metadataArray = AppInstaller::JSON::GetRawJsonArrayFromJsonNode(json, fields.Metadata);
    559         if (metadataArray)
    560         {
    561             for (const auto& item : metadataArray->get())
    562             {
    563                 std::string installerHashString = GetRequiredString(item, fields.InstallerHash);
    564                 THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, InstallerMetadataMap.find(installerHashString) != InstallerMetadataMap.end());
    565 
    566                 InstallerMetadata installerMetadata;
    567 
    568                 installerMetadata.SubmissionIdentifier = GetRequiredString(item, fields.SubmissionIdentifier);
    569                 if (submissionIdentifierVerification.empty())
    570                 {
    571                     submissionIdentifierVerification = installerMetadata.SubmissionIdentifier;
    572                 }
    573                 else if (submissionIdentifierVerification != installerMetadata.SubmissionIdentifier)
    574                 {
    575                     AICLI_LOG(Repo, Error, << "Different submission identifiers found in metadata: '" <<
    576                         submissionIdentifierVerification << "' and '" << installerMetadata.SubmissionIdentifier << "'");
    577                     THROW_HR(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE);
    578                 }
    579 
    580                 auto appsAndFeatures = AppInstaller::JSON::GetRawJsonArrayFromJsonNode(item, fields.AppsAndFeaturesEntries);
    581                 THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, !appsAndFeatures);
    582                 installerMetadata.AppsAndFeaturesEntries = parser.DeserializeAppsAndFeaturesEntries(appsAndFeatures.value());
    583 
    584                 installerMetadata.Scope = GetStringFromFutureSchema(item, fields.Scope).value_or(std::string{});
    585 
    586                 if (!fields.InstalledFiles.empty())
    587                 {
    588                     auto installedFiles = AppInstaller::JSON::GetJsonValueFromNode(item, fields.InstalledFiles);
    589                     if (installedFiles)
    590                     {
    591                         installerMetadata.InstalledFiles = parser.DeserializeInstallationMetadata(installedFiles->get());
    592                     }
    593                 }
    594 
    595                 if (!fields.InstalledStartupLinks.empty())
    596                 {
    597                     auto startupLinks = AppInstaller::JSON::GetJsonValueFromNode(item, fields.InstalledStartupLinks);
    598                     if (startupLinks)
    599                     {
    600                         installerMetadata.StartupLinkFiles = DeserializeInstalledStartupLinks(startupLinks->get(), fields);
    601                     }
    602                 }
    603 
    604                 if (!fields.Icons.empty())
    605                 {
    606                     auto icons = AppInstaller::JSON::GetJsonValueFromNode(item, fields.Icons);
    607                     if (icons)
    608                     {
    609                         installerMetadata.Icons = DeserializeExtractedIcons(icons->get(), fields);
    610                     }
    611                 }
    612 
    613                 InstallerMetadataMap[installerHashString] = std::move(installerMetadata);
    614             }
    615         }
    616 
    617         auto historicalArray = AppInstaller::JSON::GetRawJsonArrayFromJsonNode(json, fields.Historical);
    618         if (historicalArray)
    619         {
    620             for (const auto& item : historicalArray->get())
    621             {
    622                 HistoricalMetadata historicalMetadata;
    623 
    624                 historicalMetadata.ProductVersionMin = Version{ GetRequiredString(item, fields.VersionMin) };
    625                 historicalMetadata.ProductVersionMax = Version{ GetRequiredString(item, fields.VersionMax) };
    626                 historicalMetadata.Names = AppInstaller::JSON::GetRawStringSetFromJsonNode(item, fields.Names);
    627                 historicalMetadata.Publishers = AppInstaller::JSON::GetRawStringSetFromJsonNode(item, fields.Publishers);
    628                 historicalMetadata.ProductCodes = AppInstaller::JSON::GetRawStringSetFromJsonNode(item, fields.ProductCodes);
    629                 historicalMetadata.UpgradeCodes = AppInstaller::JSON::GetRawStringSetFromJsonNode(item, fields.UpgradeCodes);
    630 
    631                 HistoricalMetadataList.emplace_back(std::move(historicalMetadata));
    632             }
    633         }
    634     }
    635 
    636     web::json::value ProductMetadata::ToJson_1_N()
    637     {
    638         AICLI_LOG(Repo, Info, << "Creating metadata JSON " << SchemaVersion.ToString() << " fields");
    639 
    640         ProductMetadataFields_1_N fields{ SchemaVersion };
    641 
    642         web::json::value result;
    643 
    644         result[fields.Version] = web::json::value::string(fields.SchemaVersion);
    645         result[fields.ProductVersionMin] = AppInstaller::JSON::GetStringValue(ProductVersionMin.ToString());
    646         result[fields.ProductVersionMax] = AppInstaller::JSON::GetStringValue(ProductVersionMax.ToString());
    647 
    648         web::json::value metadataArray = web::json::value::array();
    649         size_t metadataItemIndex = 0;
    650         for (const auto& item : InstallerMetadataMap)
    651         {
    652             web::json::value itemValue;
    653 
    654             itemValue[fields.InstallerHash] = AppInstaller::JSON::GetStringValue(item.first);
    655             itemValue[fields.SubmissionIdentifier] = AppInstaller::JSON::GetStringValue(item.second.SubmissionIdentifier);
    656             SetStringFromFutureSchema(itemValue, fields.Scope, item.second.Scope);
    657             if (!fields.InstalledFiles.empty() && item.second.InstalledFiles.has_value())
    658             {
    659                 web::json::value installationMetadata;
    660 
    661                 installationMetadata[fields.DefaultInstallLocation] = AppInstaller::JSON::GetStringValue(item.second.InstalledFiles->DefaultInstallLocation);
    662 
    663                 web::json::value installedFilesArray = web::json::value::array();
    664                 size_t installedFileIndex = 0;
    665                 for (const auto& entry : item.second.InstalledFiles->Files)
    666                 {
    667                     web::json::value entryValue;
    668                     AddFieldIfNotEmpty(entryValue, fields.InstalledFileRelativeFilePath, entry.RelativeFilePath);
    669                     AddFieldIfNotEmpty(entryValue, fields.InstalledFileInvocationParameter, entry.InvocationParameter);
    670                     AddFieldIfNotEmpty(entryValue, fields.InstalledFileDisplayName, entry.DisplayName);
    671                     entryValue[fields.InstalledFileType] = AppInstaller::JSON::GetStringValue(Manifest::InstalledFileTypeToString(entry.FileType));
    672                     if (!entry.FileSha256.empty())
    673                     {
    674                         entryValue[fields.InstalledFileSha256] = AppInstaller::JSON::GetStringValue(SHA256::ConvertToString(entry.FileSha256));
    675                     }
    676                     installedFilesArray[installedFileIndex++] = std::move(entryValue);
    677                 }
    678                 installationMetadata[fields.InstallationMetadataFiles] = std::move(installedFilesArray);
    679 
    680                 itemValue[fields.InstalledFiles] = std::move(installationMetadata);
    681             }
    682 
    683             if (!fields.InstalledStartupLinks.empty() && item.second.StartupLinkFiles.has_value())
    684             {
    685                 web::json::value startupLinkFilesArray = web::json::value::array();
    686                 size_t startupLinkFileIndex = 0;
    687                 for (const auto& entry : item.second.StartupLinkFiles.value())
    688                 {
    689                     web::json::value entryValue;
    690                     entryValue[fields.InstalledStartupLinkPath] = AppInstaller::JSON::GetStringValue(entry.RelativeFilePath);
    691                     entryValue[fields.InstalledStartupLinkType] = AppInstaller::JSON::GetStringValue(Manifest::InstalledFileTypeToString(entry.FileType));
    692 
    693                     startupLinkFilesArray[startupLinkFileIndex++] = std::move(entryValue);
    694                 }
    695 
    696                 itemValue[fields.InstalledStartupLinks] = std::move(startupLinkFilesArray);
    697             }
    698 
    699             if (!fields.Icons.empty() && !item.second.Icons.empty())
    700             {
    701                 web::json::value iconsArray = web::json::value::array();
    702                 size_t iconIndex = 0;
    703                 for (const auto& entry : item.second.Icons)
    704                 {
    705                     web::json::value entryValue;
    706                     entryValue[fields.IconContent] = AppInstaller::JSON::GetStringValue(AppInstaller::JSON::Base64Encode(entry.IconContent));
    707                     if (!entry.IconSha256.empty())
    708                     {
    709                         entryValue[fields.IconSha256] = AppInstaller::JSON::GetStringValue(SHA256::ConvertToString(entry.IconSha256));
    710                     }
    711                     entryValue[fields.IconFileType] = AppInstaller::JSON::GetStringValue(Manifest::IconFileTypeToString(entry.IconFileType));
    712                     entryValue[fields.IconTheme] = AppInstaller::JSON::GetStringValue(Manifest::IconThemeToString(entry.IconTheme));
    713                     entryValue[fields.IconResolution] = AppInstaller::JSON::GetStringValue(Manifest::IconResolutionToString(entry.IconResolution));
    714 
    715                     iconsArray[iconIndex++] = std::move(entryValue);
    716                 }
    717 
    718                 itemValue[fields.Icons] = std::move(iconsArray);
    719             }
    720 
    721             web::json::value appsAndFeaturesArray = web::json::value::array();
    722             size_t appsAndFeaturesEntryIndex = 0;
    723             for (const auto& entry : item.second.AppsAndFeaturesEntries)
    724             {
    725                 web::json::value entryValue;
    726 
    727                 AddFieldIfNotEmpty(entryValue, fields.DisplayName, entry.DisplayName);
    728                 AddFieldIfNotEmpty(entryValue, fields.Publisher, entry.Publisher);
    729                 AddFieldIfNotEmpty(entryValue, fields.DisplayVersion, entry.DisplayVersion);
    730                 AddFieldIfNotEmpty(entryValue, fields.ProductCode, entry.ProductCode);
    731                 AddFieldIfNotEmpty(entryValue, fields.UpgradeCode, entry.UpgradeCode);
    732                 if (entry.InstallerType != Manifest::InstallerTypeEnum::Unknown)
    733                 {
    734                     entryValue[fields.InstallerType] = AppInstaller::JSON::GetStringValue(Manifest::InstallerTypeToString(entry.InstallerType));
    735                 }
    736 
    737                 appsAndFeaturesArray[appsAndFeaturesEntryIndex++] = std::move(entryValue);
    738             }
    739 
    740             itemValue[fields.AppsAndFeaturesEntries] = std::move(appsAndFeaturesArray);
    741 
    742             metadataArray[metadataItemIndex++] = std::move(itemValue);
    743         }
    744 
    745         result[fields.Metadata] = std::move(metadataArray);
    746 
    747         web::json::value historicalArray = web::json::value::array();
    748         size_t historicalItemIndex = 0;
    749         for (const auto& item : HistoricalMetadataList)
    750         {
    751             web::json::value itemValue;
    752 
    753             itemValue[fields.VersionMin] = AppInstaller::JSON::GetStringValue(item.ProductVersionMin.ToString());
    754             itemValue[fields.VersionMax] = AppInstaller::JSON::GetStringValue(item.ProductVersionMax.ToString());
    755             itemValue[fields.Names] = CreateStringArray(item.Names);
    756             itemValue[fields.Publishers] = CreateStringArray(item.Publishers);
    757             itemValue[fields.ProductCodes] = CreateStringArray(item.ProductCodes);
    758             itemValue[fields.UpgradeCodes] = CreateStringArray(item.UpgradeCodes);
    759 
    760             historicalArray[historicalItemIndex++] = std::move(itemValue);
    761         }
    762 
    763         result[fields.Historical] = std::move(historicalArray);
    764 
    765         return result;
    766     }
    767 
    768     bool ProductMetadata::DropOldestHistoricalData()
    769     {
    770         if (HistoricalMetadataList.empty())
    771         {
    772             return false;
    773         }
    774 
    775         HistoricalMetadataList.pop_back();
    776         return true;
    777     }
    778 
    779     InstallerMetadataCollectionContext::InstallerMetadataCollectionContext() :
    780         m_correlationData(std::make_unique<Correlation::ARPCorrelationData>()),
    781         m_installedFilesCorrelation(std::make_unique<Correlation::InstalledFilesCorrelation>())
    782     {}
    783 
    784     InstallerMetadataCollectionContext::InstallerMetadataCollectionContext(
    785         std::unique_ptr<Correlation::ARPCorrelationData> correlationData,
    786         std::unique_ptr<Correlation::InstalledFilesCorrelation> installedFilesCorrelation,
    787         const std::wstring& json) :
    788         m_correlationData(std::move(correlationData)), m_installedFilesCorrelation(std::move(installedFilesCorrelation))
    789     {
    790         auto threadGlobalsLifetime = InitializeLogging({});
    791         InitializePreinstallState(json);
    792     }
    793 
    794     std::unique_ptr<InstallerMetadataCollectionContext> InstallerMetadataCollectionContext::FromFile(const std::filesystem::path& file, const std::filesystem::path& logFile)
    795     {
    796         THROW_HR_IF(E_INVALIDARG, file.empty());
    797         THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND), !std::filesystem::exists(file));
    798 
    799         std::unique_ptr<InstallerMetadataCollectionContext> result = std::make_unique<InstallerMetadataCollectionContext>();
    800         auto threadGlobalsLifetime = result->InitializeLogging(logFile);
    801 
    802         AICLI_LOG(Repo, Info, << "Opening InstallerMetadataCollectionContext input file: " << file);
    803         std::ifstream fileStream{ file };
    804 
    805         auto content = ReadEntireStream(fileStream);
    806         // CppRestSdk's implementation of json parsing does not work with '\0', so trimming them here
    807         content.erase(std::find(content.begin(), content.end(), '\0'), content.end());
    808 
    809         result->InitializePreinstallState(ConvertToUTF16(content));
    810 
    811         return result;
    812     }
    813 
    814     std::unique_ptr<InstallerMetadataCollectionContext> InstallerMetadataCollectionContext::FromURI(std::wstring_view uri, const std::filesystem::path& logFile)
    815     {
    816         THROW_HR_IF(E_INVALIDARG, uri.empty());
    817 
    818         std::unique_ptr<InstallerMetadataCollectionContext> result = std::make_unique<InstallerMetadataCollectionContext>();
    819         auto threadGlobalsLifetime = result->InitializeLogging(logFile);
    820 
    821         std::string utf8Uri = ConvertToUTF8(uri);
    822         THROW_HR_IF(E_INVALIDARG, !IsUrlRemote(utf8Uri));
    823 
    824         AICLI_LOG(Repo, Info, << "Downloading InstallerMetadataCollectionContext input file: " << utf8Uri);
    825 
    826         std::ostringstream jsonStream;
    827         ProgressCallback emptyCallback;
    828 
    829         const int MaxRetryCount = 2;
    830         for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount)
    831         {
    832             try
    833             {
    834                 auto downloadHash = DownloadToStream(utf8Uri, jsonStream, DownloadType::InstallerMetadataCollectionInput, emptyCallback);
    835                 break;
    836             }
    837             catch (...)
    838             {
    839                 if (retryCount < MaxRetryCount - 1)
    840                 {
    841                     AICLI_LOG(Repo, Info, << "  Downloading InstallerMetadataCollectionContext input failed, waiting a bit and retrying...");
    842                     Sleep(500);
    843                 }
    844                 else
    845                 {
    846                     throw;
    847                 }
    848             }
    849         }
    850 
    851         result->InitializePreinstallState(ConvertToUTF16(jsonStream.str()));
    852 
    853         return result;
    854     }
    855 
    856     std::unique_ptr<InstallerMetadataCollectionContext> InstallerMetadataCollectionContext::FromJSON(const std::wstring& json, const std::filesystem::path& logFile)
    857     {
    858         THROW_HR_IF(E_INVALIDARG, json.empty());
    859 
    860         std::unique_ptr<InstallerMetadataCollectionContext> result = std::make_unique<InstallerMetadataCollectionContext>();
    861         auto threadGlobalsLifetime = result->InitializeLogging(logFile);
    862         result->InitializePreinstallState(json);
    863 
    864         return result;
    865     }
    866 
    867     void InstallerMetadataCollectionContext::Complete(const std::filesystem::path& output)
    868     {
    869         auto threadGlobalsLifetime = m_threadGlobals.SetForCurrentThread();
    870 
    871         THROW_HR_IF(E_INVALIDARG, !output.has_filename());
    872 
    873         if (output.has_parent_path())
    874         {
    875             std::filesystem::create_directories(output.parent_path());
    876         }
    877 
    878         std::ofstream outputStream{ output };
    879         THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_OPEN_FAILED), !outputStream);
    880 
    881         CompleteWithThreadGlobalsSet(outputStream);
    882     }
    883 
    884     void InstallerMetadataCollectionContext::Complete(std::ostream& output)
    885     {
    886         auto threadGlobalsLifetime = m_threadGlobals.SetForCurrentThread();
    887         CompleteWithThreadGlobalsSet(output);
    888     }
    889 
    890     std::wstring InstallerMetadataCollectionContext::Merge(const std::wstring& json, size_t maximumSizeInBytes, const std::filesystem::path& logFile)
    891     {
    892         ThreadLocalStorage::WingetThreadGlobals threadGlobals;
    893         auto globalsLifetime = InitializeLogging(threadGlobals, logFile);
    894 
    895         AICLI_LOG(Repo, Info, << "Parsing input JSON:\n" << ConvertToUTF8(json));
    896 
    897         // Parse and validate JSON
    898         try
    899         {
    900             utility::string_t versionFieldName = L"version";
    901 
    902             web::json::value inputValue = web::json::value::parse(json);
    903 
    904             THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, inputValue.is_null());
    905 
    906             Version inputVersion = Version{ GetRequiredString(inputValue, versionFieldName) };
    907             AICLI_LOG(Repo, Info, << "Parsing input JSON version " << inputVersion.ToString());
    908 
    909             web::json::value mergedResult;
    910 
    911             if (inputVersion.PartAt(0).Integer == 1)
    912             {
    913                 mergedResult = Merge_1_0(inputValue, maximumSizeInBytes);
    914             }
    915             else
    916             {
    917                 AICLI_LOG(Repo, Error, << "Don't know how to handle version " << inputVersion.ToString());
    918                 THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE));
    919             }
    920 
    921             std::wostringstream outputStream;
    922             mergedResult.serialize(outputStream);
    923 
    924             return std::move(outputStream).str();
    925         }
    926         catch (const web::json::json_exception& exc)
    927         {
    928             AICLI_LOG(Repo, Error, << "Exception parsing input JSON: " << exc.what());
    929         }
    930 
    931         // We will return within the try or throw a non-json exception, so if we get here it was a json exception.
    932         THROW_HR(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE);
    933     }
    934 
    935     void InstallerMetadataCollectionContext::CompleteWithThreadGlobalsSet(std::ostream& output)
    936     {
    937         web::json::value outputJSON;
    938 
    939         if (!ContainsError())
    940         {
    941             try
    942             {
    943                 // Collect post-install system state
    944                 m_correlationData->CapturePostInstallSnapshot();
    945                 m_installedFilesCorrelation->StopFileWatcher();
    946 
    947                 ComputeOutputData();
    948 
    949                 // Construct output JSON
    950                 AICLI_LOG(Repo, Info, << "Creating output JSON version for input version " << m_inputVersion.ToString());
    951 
    952                 if (m_inputVersion.PartAt(0).Integer == 1)
    953                 {
    954                     // We only have one version currently, so use that as long as the major version is 1
    955                     outputJSON = CreateOutputJson_1_0();
    956                 }
    957                 else
    958                 {
    959                     AICLI_LOG(Repo, Error, << "Don't know how to output for version " << m_inputVersion.ToString());
    960                     THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE));
    961                 }
    962             }
    963             catch (...)
    964             {
    965                 CollectErrorDataFromException(std::current_exception());
    966             }
    967         }
    968 
    969         if (ContainsError())
    970         {
    971             // We only have one version currently
    972             outputJSON = CreateErrorJson_1_0();
    973         }
    974 
    975         // Write output
    976         outputJSON.serialize(output);
    977     }
    978 
    979     std::unique_ptr<ThreadLocalStorage::PreviousThreadGlobals> InstallerMetadataCollectionContext::InitializeLogging(ThreadLocalStorage::WingetThreadGlobals& threadGlobals, const std::filesystem::path& logFile)
    980     {
    981         auto threadGlobalsLifetime = threadGlobals.SetForCurrentThread();
    982 
    983         Logging::Log().SetLevel(Logging::Level::Info);
    984         Logging::Log().SetEnabledChannels(Logging::Channel::All);
    985         Logging::EnableWilFailureTelemetry();
    986         Logging::TraceLogger::Add();
    987 
    988         if (!logFile.empty())
    989         {
    990             Logging::FileLogger::Add(logFile);
    991         }
    992 
    993         Logging::Telemetry().SetCaller("installer-metadata-collection");
    994         Logging::Telemetry().LogStartup();
    995 
    996         return threadGlobalsLifetime;
    997     }
    998 
    999     std::unique_ptr<ThreadLocalStorage::PreviousThreadGlobals> InstallerMetadataCollectionContext::InitializeLogging(const std::filesystem::path& logFile)
   1000     {
   1001         return InitializeLogging(m_threadGlobals, logFile);
   1002     }
   1003 
   1004     void InstallerMetadataCollectionContext::InitializePreinstallState(const std::wstring& json)
   1005     {
   1006         try
   1007         {
   1008             AICLI_LOG(Repo, Info, << "Parsing input JSON:\n" << ConvertToUTF8(json));
   1009 
   1010             // Parse and validate JSON
   1011             try
   1012             {
   1013                 utility::string_t versionFieldName = L"version";
   1014 
   1015                 web::json::value inputValue = web::json::value::parse(json);
   1016 
   1017                 THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, inputValue.is_null());
   1018 
   1019                 m_inputVersion = Version{ GetRequiredString(inputValue, versionFieldName) };
   1020                 AICLI_LOG(Repo, Info, << "Parsing input JSON version " << m_inputVersion.ToString());
   1021 
   1022                 if (m_inputVersion.PartAt(0).Integer == 1)
   1023                 {
   1024                     // We only have one version currently, so use that as long as the major version is 1
   1025                     ParseInputJson_1_0(inputValue);
   1026                 }
   1027                 else
   1028                 {
   1029                     AICLI_LOG(Repo, Error, << "Don't know how to handle version " << m_inputVersion.ToString());
   1030                     THROW_HR(HRESULT_FROM_WIN32(ERROR_UNSUPPORTED_TYPE));
   1031                 }
   1032             }
   1033             catch (const web::json::json_exception& exc)
   1034             {
   1035                 AICLI_LOG(Repo, Error, << "Exception parsing input JSON: " << exc.what());
   1036                 throw;
   1037             }
   1038 
   1039             // Collect pre-install system state
   1040             m_correlationData->CapturePreInstallSnapshot();
   1041             m_installedFilesCorrelation->StartFileWatcher();
   1042         }
   1043         catch (...)
   1044         {
   1045             CollectErrorDataFromException(std::current_exception());
   1046         }
   1047     }
   1048 
   1049     void InstallerMetadataCollectionContext::ComputeOutputData()
   1050     {
   1051         // Copy the metadata from the current; this function takes care of moving data to historical if the submission is new.
   1052         m_outputMetadata.CopyFrom(m_currentMetadata, m_submissionIdentifier);
   1053 
   1054         Correlation::ARPCorrelationSettings settings;
   1055         std::string arpInstallLocation;
   1056         // As this code is typically run in a controlled environment, we can assume that a single value change is very likely the correct value.
   1057         settings.AllowSingleChange = true;
   1058 
   1059         // ARP entry correlation
   1060         Correlation::ARPCorrelationResult correlationResult = m_correlationData->CorrelateForNewlyInstalled(m_incomingManifest, settings);
   1061 
   1062         if (correlationResult.Package)
   1063         {
   1064             auto& package = correlationResult.Package;
   1065 
   1066             // Update min and max versions based on the version of the correlated package
   1067             Version packageVersion{ package->GetProperty(PackageVersionProperty::Version) };
   1068 
   1069             if (m_outputMetadata.ProductVersionMin.IsEmpty() || packageVersion < m_outputMetadata.ProductVersionMin)
   1070             {
   1071                 m_outputMetadata.ProductVersionMin = packageVersion;
   1072             }
   1073 
   1074             if (m_outputMetadata.ProductVersionMax.IsEmpty() || m_outputMetadata.ProductVersionMax < packageVersion)
   1075             {
   1076                 m_outputMetadata.ProductVersionMax = packageVersion;
   1077             }
   1078 
   1079             // Create the AppsAndFeaturesEntry that we need to add
   1080             Manifest::AppsAndFeaturesEntry newEntry;
   1081             auto packageMetadata = package->GetMetadata();
   1082 
   1083             // Arp installed location will be used in later installed files correlation.
   1084             arpInstallLocation = packageMetadata[PackageVersionMetadata::InstalledLocation];
   1085 
   1086             // TODO: Use some amount of normalization here to prevent things like versions being in the name from bloating the data
   1087             newEntry.DisplayName = package->GetProperty(PackageVersionProperty::Name).get();
   1088             newEntry.DisplayVersion = packageVersion.ToString();
   1089             if (packageMetadata.count(PackageVersionMetadata::InstalledType))
   1090             {
   1091                 newEntry.InstallerType = Manifest::ConvertToInstallerTypeEnum(packageMetadata[PackageVersionMetadata::InstalledType]);
   1092             }
   1093             auto productCodes = package->GetMultiProperty(PackageVersionMultiProperty::ProductCode);
   1094             if (!productCodes.empty())
   1095             {
   1096                 newEntry.ProductCode = std::move(productCodes[0]).get();
   1097             }
   1098             newEntry.Publisher = package->GetProperty(PackageVersionProperty::Publisher).get();
   1099             // TODO: Support upgrade code throughout the code base...
   1100 
   1101             Manifest::ScopeEnum scope = Manifest::ConvertToScopeEnum(packageMetadata[PackageVersionMetadata::InstalledScope]);
   1102 
   1103             // ARP entry icon extraction upon ARP correlation success
   1104             auto icons = ExtractIconFromArpEntry(newEntry.ProductCode, scope);
   1105 
   1106             // Add or update the metadata for the installer hash
   1107             auto itr = m_outputMetadata.InstallerMetadataMap.find(m_installerHash);
   1108 
   1109             if (itr == m_outputMetadata.InstallerMetadataMap.end())
   1110             {
   1111                 // New entry needed
   1112                 ProductMetadata::InstallerMetadata newMetadata;
   1113 
   1114                 newMetadata.SubmissionIdentifier = m_submissionIdentifier;
   1115                 newMetadata.AppsAndFeaturesEntries.emplace_back(std::move(newEntry));
   1116 
   1117                 if (scope != Manifest::ScopeEnum::Unknown)
   1118                 {
   1119                     newMetadata.Scope = Manifest::ScopeToString(scope);
   1120                 }
   1121 
   1122                 if (!icons.empty())
   1123                 {
   1124                     newMetadata.Icons = std::move(icons);
   1125                 }
   1126 
   1127                 m_outputMetadata.InstallerMetadataMap[m_installerHash] = std::move(newMetadata);
   1128             }
   1129             else
   1130             {
   1131                 if (itr->second.Scope.empty())
   1132                 {
   1133                     itr->second.Scope = Manifest::ScopeToString(scope);
   1134                 }
   1135                 // If there is a conflicting scope already present, force it to Unknown
   1136                 else if (scope != Manifest::ScopeEnum::Unknown && Manifest::ConvertToScopeEnum(itr->second.Scope) != scope)
   1137                 {
   1138                     itr->second.Scope = Manifest::ScopeToString(Manifest::ScopeEnum::Unknown);
   1139                 }
   1140 
   1141                 // We will always use the latest extracted icons upon confliction.
   1142                 if (!icons.empty())
   1143                 {
   1144                     itr->second.Icons = std::move(icons);
   1145                 }
   1146 
   1147                 // Existing entry for installer hash, add/update the entry
   1148                 FilterAndAddToEntries(std::move(newEntry), itr->second.AppsAndFeaturesEntries);
   1149             }
   1150         }
   1151 
   1152         // Installation files correlation
   1153         auto installationMetadata = m_installedFilesCorrelation->CorrelateForNewlyInstalled(m_incomingManifest, arpInstallLocation);
   1154 
   1155         if (installationMetadata.InstalledFiles.HasData() || !installationMetadata.StartupLinkFiles.empty())
   1156         {
   1157             // Add or update the metadata for the installer hash
   1158             auto itr = m_outputMetadata.InstallerMetadataMap.find(m_installerHash);
   1159 
   1160             if (itr == m_outputMetadata.InstallerMetadataMap.end())
   1161             {
   1162                 // New entry needed
   1163                 ProductMetadata::InstallerMetadata newMetadata;
   1164 
   1165                 newMetadata.SubmissionIdentifier = m_submissionIdentifier;
   1166 
   1167                 if (installationMetadata.InstalledFiles.HasData())
   1168                 {
   1169                     newMetadata.InstalledFiles = std::move(installationMetadata.InstalledFiles);
   1170                 }
   1171                 if (!installationMetadata.StartupLinkFiles.empty())
   1172                 {
   1173                     newMetadata.StartupLinkFiles = std::move(installationMetadata.StartupLinkFiles);
   1174                 }
   1175 
   1176                 m_outputMetadata.InstallerMetadataMap[m_installerHash] = std::move(newMetadata);
   1177             }
   1178             else
   1179             {
   1180                 // Add new or merge with existing entry
   1181                 if (installationMetadata.InstalledFiles.HasData())
   1182                 {
   1183                     if (!itr->second.InstalledFiles.has_value())
   1184                     {
   1185                         itr->second.InstalledFiles = std::move(installationMetadata.InstalledFiles);
   1186                     }
   1187                     else
   1188                     {
   1189                         MergeInstalledFilesMetadata(*(itr->second.InstalledFiles), installationMetadata.InstalledFiles);
   1190                     }
   1191                 }
   1192 
   1193                 if (!installationMetadata.StartupLinkFiles.empty())
   1194                 {
   1195                     if (!itr->second.StartupLinkFiles.has_value())
   1196                     {
   1197                         itr->second.StartupLinkFiles = std::move(installationMetadata.StartupLinkFiles);
   1198                     }
   1199                     else
   1200                     {
   1201                         MergeStartupLinkFilesMetadata(*(itr->second.StartupLinkFiles), installationMetadata.StartupLinkFiles);
   1202                     }
   1203                 }
   1204             }
   1205         }
   1206 
   1207         if (correlationResult.Package)
   1208         {
   1209             m_outputStatus = OutputStatus::Success;
   1210         }
   1211         else
   1212         {
   1213             m_outputStatus = OutputStatus::LowConfidence;
   1214         }
   1215 
   1216         // Create the diagnostics data, based on the other values from the correlation result.
   1217         DiagnosticFields fields;
   1218 
   1219         m_outputDiagnostics[fields.Reason] = AppInstaller::JSON::GetStringValue(correlationResult.Reason);
   1220         m_outputDiagnostics[fields.ChangedEntryCount] = web::json::value::number(static_cast<int64_t>(correlationResult.ChangesToARP));
   1221         m_outputDiagnostics[fields.MatchedEntryCount] = web::json::value::number(static_cast<int64_t>(correlationResult.MatchesInARP));
   1222         m_outputDiagnostics[fields.IntersectionCount] = web::json::value::number(static_cast<int64_t>(correlationResult.CountOfIntersectionOfChangesAndMatches));
   1223 
   1224         constexpr size_t MaximumDiagnosticMeasures = 10;
   1225         web::json::value measuresArray = web::json::value::array();
   1226         for (size_t i = 0; i < correlationResult.Measures.size() && i < MaximumDiagnosticMeasures; ++i)
   1227         {
   1228             web::json::value measureValue;
   1229             const auto& measure = correlationResult.Measures[i];
   1230 
   1231             measureValue[fields.Value] = web::json::value::number(measure.Measure);
   1232             measureValue[fields.Name] = AppInstaller::JSON::GetStringValue(measure.Package->GetProperty(PackageVersionProperty::Name));
   1233             measureValue[fields.Publisher] = AppInstaller::JSON::GetStringValue(measure.Package->GetProperty(PackageVersionProperty::Publisher));
   1234 
   1235             measuresArray[i] = std::move(measureValue);
   1236         }
   1237 
   1238         m_outputDiagnostics[fields.CorrelationMeasures] = std::move(measuresArray);
   1239     }
   1240 
   1241     void InstallerMetadataCollectionContext::ParseInputJson_1_0(web::json::value& input)
   1242     {
   1243         AICLI_LOG(Repo, Info, << "Parsing input JSON 1.0 fields");
   1244 
   1245         // Field names
   1246         utility::string_t metadataVersionFieldName = L"supportedMetadataVersion";
   1247         utility::string_t metadataFieldName = L"currentMetadata";
   1248         utility::string_t submissionDataFieldName = L"submissionData";
   1249         utility::string_t submissionIdentifierFieldName = L"submissionIdentifier";
   1250         utility::string_t packageDataFieldName = L"packageData";
   1251         utility::string_t installerHashFieldName = L"installerHash";
   1252         utility::string_t defaultLocaleFieldName = L"DefaultLocale";
   1253         utility::string_t localesFieldName = L"Locales";
   1254 
   1255         // root fields
   1256         m_supportedMetadataVersion = Version{ GetRequiredString(input, metadataVersionFieldName) };
   1257 
   1258         auto currentMetadataValue = AppInstaller::JSON::GetJsonValueFromNode(input, metadataFieldName);
   1259         if (currentMetadataValue)
   1260         {
   1261             m_currentMetadata.FromJson(currentMetadataValue.value());
   1262         }
   1263 
   1264         // submissionData fields
   1265         auto submissionDataValue = AppInstaller::JSON::GetJsonValueFromNode(input, submissionDataFieldName);
   1266         THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, !submissionDataValue);
   1267         m_submissionData = submissionDataValue.value();
   1268 
   1269         m_submissionIdentifier = GetRequiredString(m_submissionData, submissionIdentifierFieldName);
   1270 
   1271         // packageData fields
   1272         auto packageDataValue = AppInstaller::JSON::GetJsonValueFromNode(input, packageDataFieldName);
   1273         THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, !packageDataValue);
   1274 
   1275         m_installerHash = GetRequiredString(packageDataValue.value(), installerHashFieldName);
   1276 
   1277         // The 1.0 version of input uses the 1.5 version of REST
   1278         JSON::ManifestJSONParser parser{ Version{ "1.5" }};
   1279 
   1280         {
   1281             auto defaultLocaleValue = AppInstaller::JSON::GetJsonValueFromNode(packageDataValue.value(), defaultLocaleFieldName);
   1282             THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, !defaultLocaleValue);
   1283 
   1284             auto defaultLocale = parser.DeserializeLocale(defaultLocaleValue.value());
   1285             THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE,
   1286                 !defaultLocale ||
   1287                 !defaultLocale->Contains(Manifest::Localization::PackageName) ||
   1288                 !defaultLocale->Contains(Manifest::Localization::Publisher));
   1289 
   1290             m_incomingManifest.DefaultLocalization = std::move(defaultLocale).value();
   1291 
   1292             auto localesArray = AppInstaller::JSON::GetRawJsonArrayFromJsonNode(packageDataValue.value(), localesFieldName);
   1293             if (localesArray)
   1294             {
   1295                 for (const auto& locale : localesArray->get())
   1296                 {
   1297                     auto localization = parser.DeserializeLocale(locale);
   1298                     if (localization)
   1299                     {
   1300                         m_incomingManifest.Localizations.emplace_back(std::move(localization).value());
   1301                     }
   1302                 }
   1303             }
   1304         }
   1305     }
   1306 
   1307     web::json::value InstallerMetadataCollectionContext::CreateOutputJson_1_0()
   1308     {
   1309         AICLI_LOG(Repo, Info, << "Setting output JSON 1.0 fields");
   1310 
   1311         OutputFields_1_0 fields;
   1312 
   1313         web::json::value result;
   1314 
   1315         result[fields.Version] = web::json::value::string(L"1.0");
   1316         result[fields.SubmissionData] = m_submissionData;
   1317         result[fields.InstallerHash] = AppInstaller::JSON::GetStringValue(m_installerHash);
   1318 
   1319         // Limit output status to 1.0 known values
   1320         OutputStatus statusToUse = OutputStatus::Unknown;
   1321         if (m_outputStatus == OutputStatus::Success || m_outputStatus == OutputStatus::Error || m_outputStatus == OutputStatus::LowConfidence)
   1322         {
   1323             statusToUse = m_outputStatus;
   1324         }
   1325         result[fields.Status] = web::json::value::string(ToString(statusToUse));
   1326 
   1327         if (m_outputStatus == OutputStatus::Success)
   1328         {
   1329             result[fields.Metadata] = m_outputMetadata.ToJson(m_supportedMetadataVersion, 0);
   1330         }
   1331 
   1332         result[fields.Diagnostics] = m_outputDiagnostics;
   1333 
   1334         return result;
   1335     }
   1336 
   1337     utility::string_t InstallerMetadataCollectionContext::ToString(OutputStatus status)
   1338     {
   1339         switch (status)
   1340         {
   1341         case OutputStatus::Success: return L"Success";
   1342         case OutputStatus::Error: return L"Error";
   1343         case OutputStatus::LowConfidence: return L"LowConfidence";
   1344         }
   1345 
   1346         // For both the status value of Unknown and anything else
   1347         return L"Unknown";
   1348     }
   1349 
   1350     bool InstallerMetadataCollectionContext::ContainsError() const
   1351     {
   1352         return m_outputStatus == OutputStatus::Error;
   1353     }
   1354 
   1355     void InstallerMetadataCollectionContext::CollectErrorDataFromException(std::exception_ptr exception)
   1356     {
   1357         m_outputStatus = OutputStatus::Error;
   1358 
   1359         try
   1360         {
   1361             std::rethrow_exception(exception);
   1362         }
   1363         catch (const wil::ResultException& re)
   1364         {
   1365             m_errorHR = re.GetErrorCode();
   1366             m_errorText = GetUserPresentableMessage(re);
   1367         }
   1368         catch (const winrt::hresult_error& hre)
   1369         {
   1370             m_errorHR = hre.code();
   1371             m_errorText = GetUserPresentableMessage(hre);
   1372         }
   1373         catch (const std::exception& e)
   1374         {
   1375             m_errorHR = E_FAIL;
   1376             m_errorText = GetUserPresentableMessage(e);
   1377         }
   1378         catch (...)
   1379         {
   1380             m_errorHR = E_UNEXPECTED;
   1381             m_errorText = "An unexpected exception type was thrown.";
   1382         }
   1383     }
   1384 
   1385     web::json::value InstallerMetadataCollectionContext::CreateErrorJson_1_0()
   1386     {
   1387         AICLI_LOG(Repo, Info, << "Setting error JSON 1.0 fields");
   1388 
   1389         OutputFields_1_0 fields;
   1390         DiagnosticFields diagnosticFields;
   1391 
   1392         web::json::value result;
   1393 
   1394         result[fields.Version] = web::json::value::string(L"1.0");
   1395         result[fields.SubmissionData] = m_submissionData;
   1396         result[fields.InstallerHash] = AppInstaller::JSON::GetStringValue(m_installerHash);
   1397         result[fields.Status] = web::json::value::string(ToString(OutputStatus::Error));
   1398         result[fields.Metadata] = web::json::value::null();
   1399 
   1400         web::json::value error;
   1401 
   1402         error[diagnosticFields.ErrorHR] = web::json::value::number(static_cast<int64_t>(m_errorHR));
   1403         error[diagnosticFields.ErrorText] = AppInstaller::JSON::GetStringValue(m_errorText);
   1404 
   1405         result[fields.Diagnostics] = std::move(error);
   1406 
   1407         return result;
   1408     }
   1409 
   1410     web::json::value InstallerMetadataCollectionContext::Merge_1_0(web::json::value& input, size_t maximumSizeInBytes)
   1411     {
   1412         AICLI_LOG(Repo, Info, << "Merging 1.0 input metadatas");
   1413 
   1414         utility::string_t metadatasFieldName = L"metadatas";
   1415 
   1416         auto metadatasValue = AppInstaller::JSON::GetRawJsonArrayFromJsonNode(input, metadatasFieldName);
   1417         THROW_HR_IF(APPINSTALLER_CLI_ERROR_JSON_INVALID_FILE, !metadatasValue);
   1418 
   1419         std::vector<ProductMetadata> metadatas;
   1420         for (const auto& value : metadatasValue->get())
   1421         {
   1422             ProductMetadata current;
   1423             current.FromJson(value);
   1424             metadatas.emplace_back(std::move(current));
   1425         }
   1426 
   1427         THROW_HR_IF(E_NOT_SET, metadatas.empty());
   1428 
   1429         // Require that all merging values use the same submission
   1430         for (const ProductMetadata& metadata : metadatas)
   1431         {
   1432             const std::string& firstSubmission = metadatas[0].InstallerMetadataMap.begin()->second.SubmissionIdentifier;
   1433             const std::string& metadataSubmission = metadata.InstallerMetadataMap.begin()->second.SubmissionIdentifier;
   1434             if (firstSubmission != metadataSubmission)
   1435             {
   1436                 AICLI_LOG(Repo, Info, << "Found submission identifier mismatch: " << firstSubmission << " != " << metadataSubmission);
   1437                 THROW_HR(E_NOT_VALID_STATE);
   1438             }
   1439         }
   1440 
   1441         // Do the actual merging
   1442         ProductMetadata resultMetadata;
   1443 
   1444         // The historical data should be the same across the board, so we can just copy the first one.
   1445         resultMetadata.HistoricalMetadataList = metadatas[0].HistoricalMetadataList;
   1446 
   1447         for (const ProductMetadata& metadata : metadatas)
   1448         {
   1449             // Get the minimum and maximum versions from the individual values
   1450             if (resultMetadata.ProductVersionMin.IsEmpty() || metadata.ProductVersionMin < resultMetadata.ProductVersionMin)
   1451             {
   1452                 resultMetadata.ProductVersionMin = metadata.ProductVersionMin;
   1453             }
   1454 
   1455             if (resultMetadata.ProductVersionMax < metadata.ProductVersionMax)
   1456             {
   1457                 resultMetadata.ProductVersionMax = metadata.ProductVersionMax;
   1458             }
   1459 
   1460             if (resultMetadata.SchemaVersion < metadata.SchemaVersion)
   1461             {
   1462                 resultMetadata.SchemaVersion = metadata.SchemaVersion;
   1463             }
   1464 
   1465             for (const auto& installerMetadata : metadata.InstallerMetadataMap)
   1466             {
   1467                 auto itr = resultMetadata.InstallerMetadataMap.find(installerMetadata.first);
   1468                 if (itr == resultMetadata.InstallerMetadataMap.end())
   1469                 {
   1470                     // Installer hash not in the result, so just copy it
   1471                     resultMetadata.InstallerMetadataMap.emplace(installerMetadata);
   1472                 }
   1473                 else
   1474                 {
   1475                     if (itr->second.Scope.empty())
   1476                     {
   1477                         itr->second.Scope = installerMetadata.second.Scope;
   1478                     }
   1479                     else if (!installerMetadata.second.Scope.empty())
   1480                     {
   1481                         // If there is a conflicting scope already present, force it to Unknown
   1482                         if (Manifest::ConvertToScopeEnum(itr->second.Scope) != Manifest::ConvertToScopeEnum(installerMetadata.second.Scope))
   1483                         {
   1484                             itr->second.Scope = Manifest::ScopeToString(Manifest::ScopeEnum::Unknown);
   1485                         }
   1486                     }
   1487 
   1488                     // We will always use the latest extracted icons upon confliction.
   1489                     if (!installerMetadata.second.Icons.empty())
   1490                     {
   1491                         itr->second.Icons = installerMetadata.second.Icons;
   1492                     }
   1493 
   1494                     if (!itr->second.InstalledFiles.has_value())
   1495                     {
   1496                         itr->second.InstalledFiles = installerMetadata.second.InstalledFiles;
   1497                     }
   1498                     else if (installerMetadata.second.InstalledFiles.has_value())
   1499                     {
   1500                         MergeInstalledFilesMetadata(*(itr->second.InstalledFiles), *(installerMetadata.second.InstalledFiles));
   1501                     }
   1502 
   1503                     if (!itr->second.StartupLinkFiles.has_value())
   1504                     {
   1505                         itr->second.StartupLinkFiles = installerMetadata.second.StartupLinkFiles;
   1506                     }
   1507                     else if (installerMetadata.second.StartupLinkFiles.has_value())
   1508                     {
   1509                         MergeStartupLinkFilesMetadata(*(itr->second.StartupLinkFiles), *(installerMetadata.second.StartupLinkFiles));
   1510                     }
   1511 
   1512                     // Merge into existing installer data
   1513                     for (const auto& targetEntry : installerMetadata.second.AppsAndFeaturesEntries)
   1514                     {
   1515                         FilterAndAddToEntries(Manifest::AppsAndFeaturesEntry{ targetEntry }, itr->second.AppsAndFeaturesEntries);
   1516                     }
   1517                 }
   1518             }
   1519         }
   1520 
   1521         // Convert to JSON
   1522         return resultMetadata.ToJson(resultMetadata.SchemaVersion, maximumSizeInBytes);
   1523     }
   1524 }