winget-cli

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

PackageCollection.cpp (13202B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include "PackageCollection.h"
      5 
      6 #include <AppInstallerLogging.h>
      7 #include <AppInstallerRuntime.h>
      8 #include <winget/JsonSchemaValidation.h>
      9 
     10 #include "PackagesSchema.h"
     11 
     12 #include <algorithm>
     13 #include <ostream>
     14 
     15 using namespace AppInstaller::Repository;
     16 
     17 namespace AppInstaller::CLI
     18 {
     19     namespace
     20     {
     21         // Strings used in the Packages JSON file.
     22         // Most will be used to access a JSON value, so they need to be std::string
     23         struct StaticStrings
     24         {
     25             const std::string PackagesJson_Schema = "$schema";
     26             const std::string PackagesJson_SchemaUri_v1_0 = "https://aka.ms/winget-packages.schema.1.0.json";
     27             const std::string PackagesJson_SchemaUri_v2_0 = "https://aka.ms/winget-packages.schema.2.0.json";
     28             const std::string PackagesJson_WinGetVersion = "WinGetVersion";
     29             const std::string PackagesJson_CreationDate = "CreationDate";
     30 
     31             const std::string PackagesJson_Sources = "Sources";
     32             const std::string PackagesJson_Source_Details = "SourceDetails";
     33             const std::string PackagesJson_Source_Name = "Name";
     34             const std::string PackagesJson_Source_Identifier = "Identifier";
     35             const std::string PackagesJson_Source_Argument = "Argument";
     36             const std::string PackagesJson_Source_Type = "Type";
     37 
     38             const std::string PackagesJson_Packages = "Packages";
     39             const std::string PackagesJson_Package_Id = "Id";
     40             const std::string PackagesJson_Package_PackageIdentifier = "PackageIdentifier";
     41             const std::string PackagesJson_Package_Version = "Version";
     42             const std::string PackagesJson_Package_Channel = "Channel";
     43             const std::string PackagesJson_Package_Scope = "Scope";
     44 
     45             static const StaticStrings& Instance()
     46             {
     47                 static StaticStrings instance;
     48                 return instance;
     49             }
     50         };
     51 
     52         // Gets or creates a property of a JSON object by its name.
     53         Json::Value& GetJsonProperty(Json::Value& node, const std::string& propertyName, Json::ValueType valueType)
     54         {
     55             if (!node.isMember(propertyName))
     56             {
     57                 node[propertyName] = Json::Value{ valueType };
     58             }
     59             else
     60             {
     61                 THROW_HR_IF(E_NOT_VALID_STATE, node[propertyName].type() != valueType);
     62             }
     63 
     64             return node[propertyName];
     65         }
     66 
     67         // The interface for a package collection parser.
     68         struct IPackageCollectionParser
     69         {
     70             virtual ~IPackageCollectionParser() = default;
     71 
     72             virtual PackageCollection Parse(const Json::Value& root) = 0;
     73         };
     74 
     75         // The parsing code for schema v1.0
     76         struct PackageCollectionParser_1_0 : public IPackageCollectionParser
     77         {
     78             PackageCollection Parse(const Json::Value& root) override
     79             {
     80                 PackageCollection result;
     81 
     82                 // Regardless of the fact that the value is required in 1.0, allow it to be optional
     83                 if (root.isMember(ss.PackagesJson_WinGetVersion))
     84                 {
     85                     result.ClientVersion = root[ss.PackagesJson_WinGetVersion].asString();
     86                 }
     87 
     88                 for (const auto& sourceNode : root[ss.PackagesJson_Sources])
     89                 {
     90                     auto newSource = ParseSourceNode(sourceNode);
     91                     auto existingSource = std::find_if(result.Sources.begin(), result.Sources.end(), [&](const PackageCollection::Source& s) { return s.Details.Identifier == newSource.Details.Identifier; });
     92                     if (existingSource == result.Sources.end())
     93                     {
     94                         result.Sources.push_back(std::move(newSource));
     95                     }
     96                     else
     97                     {
     98                         existingSource->Packages.insert(existingSource->Packages.end(), newSource.Packages.begin(), newSource.Packages.end());
     99                     }
    100                 }
    101 
    102                 return result;
    103             }
    104 
    105         protected:
    106             // Reads the description of a package from a Package node in the JSON.
    107             virtual PackageCollection::Package ParsePackageNode(const Json::Value& packageNode)
    108             {
    109                 std::string id = packageNode[ss.PackagesJson_Package_Id].asString();
    110                 std::string version = packageNode.isMember(ss.PackagesJson_Package_Version) ? packageNode[ss.PackagesJson_Package_Version].asString() : "";
    111                 std::string channel = packageNode.isMember(ss.PackagesJson_Package_Channel) ? packageNode[ss.PackagesJson_Package_Channel].asString() : "";
    112                 std::string scope = packageNode.isMember(ss.PackagesJson_Package_Scope) ? packageNode[ss.PackagesJson_Package_Scope].asString() : "";
    113 
    114                 PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } };
    115                 package.Scope = Manifest::ConvertToScopeEnum(scope);
    116 
    117                 return package;
    118             }
    119 
    120             // Reads the description of a Source and all the packages needed from it, from a Source node in the JSON.
    121             PackageCollection::Source ParseSourceNode(const Json::Value& sourceNode)
    122             {
    123                 SourceDetails sourceDetails;
    124                 auto& detailsNode = sourceNode[ss.PackagesJson_Source_Details];
    125                 sourceDetails.Identifier = Utility::LocIndString{ detailsNode[ss.PackagesJson_Source_Identifier].asString() };
    126                 sourceDetails.Name = detailsNode[ss.PackagesJson_Source_Name].asString();
    127                 sourceDetails.Arg = detailsNode[ss.PackagesJson_Source_Argument].asString();
    128                 sourceDetails.Type = detailsNode[ss.PackagesJson_Source_Type].asString();
    129 
    130                 PackageCollection::Source source{ std::move(sourceDetails) };
    131                 for (const auto& packageNode : sourceNode[ss.PackagesJson_Packages])
    132                 {
    133                     source.Packages.emplace_back(ParsePackageNode(packageNode));
    134                 }
    135 
    136                 return source;
    137             }
    138 
    139             const StaticStrings& ss = StaticStrings::Instance();
    140         };
    141 
    142         // The parsing code for schema v2.0
    143         struct PackageCollectionParser_2_0 : public PackageCollectionParser_1_0
    144         {
    145         protected:
    146             // Reads the description of a package from a Package node in the JSON.
    147             PackageCollection::Package ParsePackageNode(const Json::Value& packageNode) override
    148             {
    149                 std::string id = packageNode[ss.PackagesJson_Package_PackageIdentifier].asString();
    150                 std::string version = packageNode.isMember(ss.PackagesJson_Package_Version) ? packageNode[ss.PackagesJson_Package_Version].asString() : "";
    151                 std::string channel = packageNode.isMember(ss.PackagesJson_Package_Channel) ? packageNode[ss.PackagesJson_Package_Channel].asString() : "";
    152                 std::string scope = packageNode.isMember(ss.PackagesJson_Package_Scope) ? packageNode[ss.PackagesJson_Package_Scope].asString() : "";
    153 
    154                 PackageCollection::Package package{ Utility::LocIndString{ id }, Utility::Version{ version }, Utility::Channel{ channel } };
    155                 package.Scope = Manifest::ConvertToScopeEnum(scope);
    156 
    157                 return package;
    158             }
    159         };
    160 
    161         // Creates a minimal root object of a Packages JSON file.
    162         Json::Value CreateRoot(const std::string& wingetVersion)
    163         {
    164             const auto& ss = StaticStrings::Instance();
    165 
    166             Json::Value root{ Json::ValueType::objectValue };
    167             root[ss.PackagesJson_WinGetVersion] = wingetVersion;
    168             // We only generate the latest schema
    169             root[ss.PackagesJson_Schema] = ss.PackagesJson_SchemaUri_v2_0;
    170 
    171             std::stringstream currentTimeStream;
    172             Utility::OutputTimePoint(currentTimeStream, std::chrono::system_clock::now(), true);
    173             root[ss.PackagesJson_CreationDate] = currentTimeStream.str();
    174 
    175             return root;
    176         }
    177 
    178         // Adds a new Package node to a Source node in the Json file, and returns it.
    179         Json::Value& AddPackageToSource(Json::Value& sourceNode, const PackageCollection::Package& package)
    180         {
    181             const auto& ss = StaticStrings::Instance();
    182 
    183             Json::Value packageNode{ Json::ValueType::objectValue };
    184             packageNode[ss.PackagesJson_Package_PackageIdentifier] = package.Id.get();
    185 
    186             // Only add version and channel if present.
    187             // Packages may not have a channel, or versions may not have been requested.
    188             const std::string& version = package.VersionAndChannel.GetVersion().ToString();
    189             if (!version.empty())
    190             {
    191                 packageNode[ss.PackagesJson_Package_Version] = version;
    192             }
    193 
    194             const std::string& channel = package.VersionAndChannel.GetChannel().ToString();
    195             if (!channel.empty())
    196             {
    197                 packageNode[ss.PackagesJson_Package_Channel] = channel;
    198             }
    199 
    200             if (package.Scope != Manifest::ScopeEnum::Unknown)
    201             {
    202                 packageNode[ss.PackagesJson_Package_Scope] = std::string{ Manifest::ScopeToString(package.Scope) };
    203             }
    204 
    205             return sourceNode[ss.PackagesJson_Packages].append(std::move(packageNode));
    206         }
    207 
    208         // Adds a new Source node to the JSON, and returns it.
    209         Json::Value& AddSourceNode(Json::Value& root, const PackageCollection::Source& source)
    210         {
    211             const auto& ss = StaticStrings::Instance();
    212 
    213             Json::Value sourceNode{ Json::ValueType::objectValue };
    214 
    215             Json::Value sourceDetailsNode{ Json::ValueType::objectValue };
    216             sourceDetailsNode[ss.PackagesJson_Source_Name] = source.Details.Name;
    217             sourceDetailsNode[ss.PackagesJson_Source_Argument] = source.Details.Arg;
    218             sourceDetailsNode[ss.PackagesJson_Source_Identifier] = source.Details.Identifier;
    219             sourceDetailsNode[ss.PackagesJson_Source_Type] = source.Details.Type;
    220             sourceNode[ss.PackagesJson_Source_Details] = std::move(sourceDetailsNode);
    221 
    222             sourceNode[ss.PackagesJson_Packages] = Json::Value{ Json::ValueType::arrayValue };
    223 
    224             auto& sourcesNode = GetJsonProperty(root, ss.PackagesJson_Sources, Json::ValueType::arrayValue);
    225             for (const auto& package : source.Packages)
    226             {
    227                 AddPackageToSource(sourceNode, package);
    228             }
    229 
    230             return sourcesNode.append(std::move(sourceNode));
    231         }
    232     }
    233 
    234     namespace PackagesJson
    235     {
    236         Json::Value CreateJson(const PackageCollection& packages)
    237         {
    238             Json::Value root = CreateRoot(packages.ClientVersion);
    239             for (const auto& source : packages.Sources)
    240             {
    241                 AddSourceNode(root, source);
    242             }
    243 
    244             return root;
    245         }
    246 
    247         ParseResult TryParseJson(const Json::Value& root)
    248         {
    249             const auto& ss = StaticStrings::Instance();
    250 
    251             // Find the schema used for the JSON
    252             if (!(root.isObject() && root.isMember(ss.PackagesJson_Schema) && root[ss.PackagesJson_Schema].isString()))
    253             {
    254                 AICLI_LOG(CLI, Error, << "Import file is missing \"" << ss.PackagesJson_Schema << "\" property");
    255                 return ParseResult{ ParseResult::Type::MissingSchema };
    256             }
    257 
    258             const auto& schemaUri = root[ss.PackagesJson_Schema].asString();
    259             Json::Value schemaJson;
    260             std::unique_ptr<IPackageCollectionParser> parser;
    261             if (schemaUri == ss.PackagesJson_SchemaUri_v1_0)
    262             {
    263                 schemaJson = JsonSchema::LoadResourceAsSchemaDoc(MAKEINTRESOURCE(IDX_PACKAGES_SCHEMA_V1), MAKEINTRESOURCE(PACKAGESSCHEMA_RESOURCE_TYPE));
    264                 parser = std::make_unique<PackageCollectionParser_1_0>();
    265             }
    266             else if (schemaUri == ss.PackagesJson_SchemaUri_v2_0)
    267             {
    268                 schemaJson = JsonSchema::LoadResourceAsSchemaDoc(MAKEINTRESOURCE(IDX_PACKAGES_SCHEMA_V2), MAKEINTRESOURCE(PACKAGESSCHEMA_RESOURCE_TYPE));
    269                 parser = std::make_unique<PackageCollectionParser_2_0>();
    270             }
    271             else
    272             {
    273                 AICLI_LOG(CLI, Error, << "Unrecognized schema for import file: " << schemaUri);
    274                 return ParseResult{ ParseResult::Type::UnrecognizedSchema };
    275             }
    276 
    277             // Validate the JSON against the schema.
    278             valijson::Schema schema;
    279             JsonSchema::PopulateSchema(schemaJson, schema);
    280 
    281             valijson::ValidationResults results;
    282             if (!JsonSchema::Validate(schema, root, results))
    283             {
    284                 return ParseResult{ ParseResult::Type::SchemaValidationFailed, JsonSchema::GetErrorStringFromResults(results) };
    285             }
    286 
    287             // Extract the data from the JSON.
    288             return ParseResult{ parser->Parse(root) };
    289         }
    290     }
    291 }