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 }