ManifestSchemaValidation.cpp (21666B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "winget/Yaml.h" 5 #include "winget/JsonSchemaValidation.h" 6 #include "winget/ManifestCommon.h" 7 #include "winget/ManifestSchemaValidation.h" 8 #include "winget/ManifestYamlParser.h" 9 #include "winget/Resources.h" 10 11 #include <ManifestSchema.h> 12 13 namespace AppInstaller::Manifest::YamlParser 14 { 15 using namespace std::string_view_literals; 16 17 namespace 18 { 19 enum class YamlScalarType 20 { 21 String, 22 Int, 23 Bool 24 }; 25 26 // List of fields that use non string scalar types 27 const std::map<std::string_view, YamlScalarType> ManifestFieldTypes = 28 { 29 { "InstallerSuccessCodes"sv, YamlScalarType::Int }, 30 { "InstallerAbortsTerminal"sv, YamlScalarType::Bool }, 31 { "InstallLocationRequired"sv, YamlScalarType::Bool }, 32 { "RequireExplicitUpgrade"sv, YamlScalarType::Bool }, 33 { "DisplayInstallWarnings"sv, YamlScalarType::Bool }, 34 { "InstallerReturnCode"sv, YamlScalarType::Int }, 35 { "DownloadCommandProhibited", YamlScalarType::Bool }, 36 { "ArchiveBinariesDependOnPath", YamlScalarType::Bool } 37 }; 38 39 YamlScalarType GetManifestScalarValueType(const std::string& key) 40 { 41 auto iter = ManifestFieldTypes.find(key); 42 if (iter != ManifestFieldTypes.end()) 43 { 44 return iter->second; 45 } 46 47 return YamlScalarType::String; 48 } 49 50 Json::Value YamlScalarNodeToJson(const YAML::Node& scalarNode, YamlScalarType scalarType) 51 { 52 if (scalarType == YamlScalarType::Int) 53 { 54 return Json::Value(scalarNode.as<int>()); 55 } 56 else if (scalarType == YamlScalarType::Bool) 57 { 58 return Json::Value(scalarNode.as<bool>()); 59 } 60 else 61 { 62 return Json::Value(scalarNode.as<std::string>()); 63 } 64 } 65 66 Json::Value ManifestYamlNodeToJson(const YAML::Node& rootNode, YamlScalarType scalarType = YamlScalarType::String) 67 { 68 Json::Value result; 69 70 if (rootNode.IsNull()) 71 { 72 result = Json::Value::nullSingleton(); 73 } 74 else if (rootNode.IsMap()) 75 { 76 for (auto const& keyValuePair : rootNode.Mapping()) 77 { 78 // We only support string type as key in our manifest 79 auto key = keyValuePair.first.as<std::string>(); 80 result[keyValuePair.first.as<std::string>()] = ManifestYamlNodeToJson(keyValuePair.second, GetManifestScalarValueType(key)); 81 } 82 } 83 else if (rootNode.IsSequence()) 84 { 85 for (auto const& value : rootNode.Sequence()) 86 { 87 result.append(ManifestYamlNodeToJson(value, scalarType)); 88 } 89 } 90 else if (rootNode.IsScalar()) 91 { 92 result = YamlScalarNodeToJson(rootNode, scalarType); 93 } 94 else 95 { 96 THROW_HR(E_UNEXPECTED); 97 } 98 99 return result; 100 } 101 102 std::vector<ValidationError> ParseSchemaHeaderString(const YamlManifestInfo& manifestInfo, const ValidationError::Level& errorLevel, std::string& schemaHeaderUrlString) 103 { 104 std::vector<ValidationError> errors; 105 std::string schemaHeader = manifestInfo.DocumentSchemaHeader.SchemaHeader; 106 107 // Remove the leading '#' and any leading/trailing whitespaces 108 if (schemaHeader[0] == '#') 109 { 110 schemaHeader = schemaHeader.substr(1); // Remove the leading '#' 111 schemaHeader = Utility::Trim(schemaHeader); // Trim leading/trailing whitespaces 112 } 113 114 // Parse the schema header string as YAML string to get the schema header URL 115 try 116 { 117 auto root = YAML::Load(schemaHeader); 118 119 if (root.IsNull() || (!root.IsNull() && !root.IsDefined())) 120 { 121 errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::InvalidSchemaHeader, "", schemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, manifestInfo.DocumentSchemaHeader.Mark.column, errorLevel, manifestInfo.FileName)); 122 } 123 else 124 { 125 schemaHeaderUrlString = root[YAML::DocumentSchemaHeader::YamlLanguageServerKey].as<std::string>(); 126 } 127 } 128 catch (const YAML::Exception&) 129 { 130 errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::InvalidSchemaHeader, "", schemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, manifestInfo.DocumentSchemaHeader.Mark.column, errorLevel, manifestInfo.FileName)); 131 } 132 catch (const std::exception&) 133 { 134 errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::InvalidSchemaHeader, "", schemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, manifestInfo.DocumentSchemaHeader.Mark.column, errorLevel, manifestInfo.FileName)); 135 } 136 137 return errors; 138 } 139 140 bool ParseSchemaHeaderUrl(const std::string& schemaHeaderValue, std::string& schemaType, std::string& schemaVersion) 141 { 142 // Use regex to match the pattern of @"winget-manifest\.(?<type>\w+)\.(?<version>[\d\.]+)\.schema\.json$" 143 std::regex schemaUrlPattern(R"(winget-manifest\.(\w+)\.([\d\.]+)\.schema\.json$)"); 144 std::smatch match; 145 146 if (std::regex_search(schemaHeaderValue, match, schemaUrlPattern)) 147 { 148 schemaType = match[1].str(); 149 schemaVersion = match[2].str(); 150 return true; 151 } 152 153 return false; 154 } 155 156 std::vector<ValidationError> ValidateSchemaHeaderType(const std::string& headerManifestType, const ManifestTypeEnum& expectedManifestType, const YamlManifestInfo& manifestInfo, ValidationError::Level errorLevel) 157 { 158 std::vector<ValidationError> errors; 159 ManifestTypeEnum actualManifestType = ConvertToManifestTypeEnum(headerManifestType); 160 size_t schemaHeaderTypeIndex = manifestInfo.DocumentSchemaHeader.SchemaHeader.find(headerManifestType) + 1; 161 162 if (actualManifestType != expectedManifestType) 163 { 164 errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::SchemaHeaderManifestTypeMismatch, "", headerManifestType, manifestInfo.DocumentSchemaHeader.Mark.line, schemaHeaderTypeIndex, errorLevel, manifestInfo.FileName)); 165 } 166 167 return errors; 168 } 169 170 std::vector<ValidationError> ValidateSchemaHeaderVersion(const std::string& headerManifestVersion, const ManifestVer& expectedManifestVersion, const YamlManifestInfo& manifestInfo, ValidationError::Level errorLevel) 171 { 172 std::vector<ValidationError> errors; 173 ManifestVer actualHeaderVersion(headerManifestVersion); 174 size_t schemaHeaderVersionIndex = manifestInfo.DocumentSchemaHeader.SchemaHeader.find(headerManifestVersion) + 1; 175 176 if (actualHeaderVersion != expectedManifestVersion) 177 { 178 errors.emplace_back(ValidationError::MessageContextValueLineLevelWithFile(ManifestError::SchemaHeaderManifestVersionMismatch, "", headerManifestVersion, manifestInfo.DocumentSchemaHeader.Mark.line, schemaHeaderVersionIndex, errorLevel, manifestInfo.FileName)); 179 } 180 181 return errors; 182 } 183 184 bool IsValidSchemaHeaderUrl(const std::string& schemaHeaderUrlString, const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion) 185 { 186 // Load the schema file to compare the schema header URL with the schema ID in the schema file 187 Json::Value schemaFile = LoadSchemaDoc(manifestVersion, manifestInfo.ManifestType); 188 189 if (schemaFile.isMember("$id")) 190 { 191 std::string schemaId = schemaFile["$id"].asString(); 192 193 // Prefix schema ID with "schema=" to match the schema header URL pattern and compare it with the schema header URL 194 schemaId = "$schema=" + schemaId; 195 196 if (Utility::CaseInsensitiveEquals(schemaId, schemaHeaderUrlString)) 197 { 198 return true; 199 } 200 } 201 202 return false; 203 } 204 205 ValidationError GetSchemaHeaderUrlPatternMismatchError(const std::string& schemaHeaderUrlString, const YamlManifestInfo& manifestInfo, const ValidationError::Level& errorLevel) 206 { 207 size_t schemaHeaderUrlIndex = manifestInfo.DocumentSchemaHeader.SchemaHeader.find(schemaHeaderUrlString) + 1; 208 209 return ValidationError::MessageContextValueLineLevelWithFile(ManifestError::SchemaHeaderUrlPatternMismatch, "", manifestInfo.DocumentSchemaHeader.SchemaHeader, manifestInfo.DocumentSchemaHeader.Mark.line, schemaHeaderUrlIndex, errorLevel, manifestInfo.FileName); 210 } 211 212 std::vector<ValidationError> ValidateSchemaHeaderUrl(const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion, const ValidationError::Level& errorLevel) 213 { 214 std::vector<ValidationError> errors; 215 216 std::string schemaHeaderUrlString; 217 // Parse the schema header string to get the schema header URL 218 auto parserErrors = ParseSchemaHeaderString(manifestInfo, errorLevel, schemaHeaderUrlString); 219 std::move(parserErrors.begin(), parserErrors.end(), std::inserter(errors, errors.end())); 220 221 if (!errors.empty()) 222 { 223 return errors; 224 } 225 226 std::string manifestTypeString; 227 std::string manifestVersionString; 228 229 // Parse the schema header URL to get the manifest type and version 230 if (ParseSchemaHeaderUrl(schemaHeaderUrlString, manifestTypeString, manifestVersionString)) 231 { 232 auto headerManifestTypeErrors = ValidateSchemaHeaderType(manifestTypeString, manifestInfo.ManifestType, manifestInfo, errorLevel); 233 std::move(headerManifestTypeErrors.begin(), headerManifestTypeErrors.end(), std::inserter(errors, errors.end())); 234 235 auto headerManifestVersionErrors = ValidateSchemaHeaderVersion(manifestVersionString, manifestVersion, manifestInfo, errorLevel); 236 std::move(headerManifestVersionErrors.begin(), headerManifestVersionErrors.end(), std::inserter(errors, errors.end())); 237 238 // Finally, match the entire schema header URL with the schema ID in the schema file to ensure the URL domain matches the schema definition file. 239 if (!IsValidSchemaHeaderUrl(schemaHeaderUrlString, manifestInfo, manifestVersion)) 240 { 241 errors.emplace_back(GetSchemaHeaderUrlPatternMismatchError(schemaHeaderUrlString, manifestInfo, errorLevel)); 242 } 243 } 244 else 245 { 246 errors.emplace_back(GetSchemaHeaderUrlPatternMismatchError(schemaHeaderUrlString, manifestInfo, errorLevel)); 247 } 248 249 return errors; 250 } 251 252 std::vector<ValidationError> ValidateYamlManifestSchemaHeader(const YamlManifestInfo& manifestInfo, const ManifestVer& manifestVersion, ValidationError::Level errorLevel) 253 { 254 std::vector<ValidationError> errors; 255 std::string schemaHeaderString; 256 257 if (manifestInfo.DocumentSchemaHeader.SchemaHeader.empty()) 258 { 259 errors.emplace_back(ValidationError::MessageLevelWithFile(ManifestError::SchemaHeaderNotFound, errorLevel, manifestInfo.FileName)); 260 return errors; 261 } 262 263 auto parserErrors = ValidateSchemaHeaderUrl(manifestInfo, manifestVersion, errorLevel); 264 std::move(parserErrors.begin(), parserErrors.end(), std::inserter(errors, errors.end())); 265 266 return errors; 267 } 268 } 269 270 Json::Value LoadSchemaDoc(const ManifestVer& manifestVersion, ManifestTypeEnum manifestType) 271 { 272 int idx = MANIFESTSCHEMA_NO_RESOURCE; 273 std::map<ManifestTypeEnum, int> resourceMap; 274 275 if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_12 }) 276 { 277 resourceMap = { 278 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_12_SINGLETON }, 279 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_12_VERSION }, 280 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_12_INSTALLER }, 281 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_12_DEFAULTLOCALE }, 282 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_12_LOCALE }, 283 }; 284 } 285 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_10 }) 286 { 287 resourceMap = { 288 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_10_SINGLETON }, 289 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_10_VERSION }, 290 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_10_INSTALLER }, 291 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_10_DEFAULTLOCALE }, 292 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_10_LOCALE }, 293 }; 294 } 295 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_9 }) 296 { 297 resourceMap = { 298 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_9_SINGLETON }, 299 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_9_VERSION }, 300 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_9_INSTALLER }, 301 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_9_DEFAULTLOCALE }, 302 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_9_LOCALE }, 303 }; 304 } 305 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_7 }) 306 { 307 resourceMap = { 308 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_7_SINGLETON }, 309 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_7_VERSION }, 310 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_7_INSTALLER }, 311 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_7_DEFAULTLOCALE }, 312 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_7_LOCALE }, 313 }; 314 } 315 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_6 }) 316 { 317 resourceMap = { 318 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_6_SINGLETON }, 319 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_6_VERSION }, 320 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_6_INSTALLER }, 321 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_6_DEFAULTLOCALE }, 322 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_6_LOCALE }, 323 }; 324 } 325 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_5 }) 326 { 327 resourceMap = { 328 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_5_SINGLETON }, 329 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_5_VERSION }, 330 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_5_INSTALLER }, 331 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_5_DEFAULTLOCALE }, 332 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_5_LOCALE }, 333 }; 334 } 335 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_4 }) 336 { 337 resourceMap = { 338 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_4_SINGLETON }, 339 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_4_VERSION }, 340 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_4_INSTALLER }, 341 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_4_DEFAULTLOCALE }, 342 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_4_LOCALE }, 343 }; 344 } 345 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_2 }) 346 { 347 resourceMap = { 348 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_2_SINGLETON }, 349 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_2_VERSION }, 350 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_2_INSTALLER }, 351 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_2_DEFAULTLOCALE }, 352 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_2_LOCALE }, 353 }; 354 } 355 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1_1 }) 356 { 357 resourceMap = { 358 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_1_SINGLETON }, 359 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_1_VERSION }, 360 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_1_INSTALLER }, 361 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_1_DEFAULTLOCALE }, 362 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_1_LOCALE }, 363 }; 364 } 365 else if (manifestVersion >= ManifestVer{ s_ManifestVersionV1 }) 366 { 367 resourceMap = { 368 { ManifestTypeEnum::Singleton, IDX_MANIFEST_SCHEMA_V1_SINGLETON }, 369 { ManifestTypeEnum::Version, IDX_MANIFEST_SCHEMA_V1_VERSION }, 370 { ManifestTypeEnum::Installer, IDX_MANIFEST_SCHEMA_V1_INSTALLER }, 371 { ManifestTypeEnum::DefaultLocale, IDX_MANIFEST_SCHEMA_V1_DEFAULTLOCALE }, 372 { ManifestTypeEnum::Locale, IDX_MANIFEST_SCHEMA_V1_LOCALE }, 373 }; 374 } 375 else 376 { 377 resourceMap = { 378 { ManifestTypeEnum::Preview, IDX_MANIFEST_SCHEMA_PREVIEW }, 379 }; 380 } 381 382 auto iter = resourceMap.find(manifestType); 383 if (iter != resourceMap.end()) 384 { 385 idx = iter->second; 386 } 387 else 388 { 389 THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); 390 } 391 392 std::string_view schemaStr = Resource::GetResourceAsString(idx, MANIFESTSCHEMA_RESOURCE_TYPE); 393 return JsonSchema::LoadSchemaDoc(schemaStr); 394 } 395 396 std::vector<ValidationError> ValidateAgainstSchema(const std::vector<YamlManifestInfo>& manifestList, const ManifestVer& manifestVersion) 397 { 398 std::vector<ValidationError> errors; 399 // A list of schema validator to avoid multiple loadings of same schema 400 std::map<ManifestTypeEnum, valijson::Schema> schemaList; 401 402 for (const auto& entry : manifestList) 403 { 404 if (entry.ManifestType == ManifestTypeEnum::Shadow) 405 { 406 // There's no schema for a shadow manifest. 407 continue; 408 } 409 410 if (schemaList.find(entry.ManifestType) == schemaList.end()) 411 { 412 // Copy constructor of valijson::Schema was private 413 valijson::Schema& newSchema = schemaList.emplace( 414 std::piecewise_construct, std::make_tuple(entry.ManifestType), std::make_tuple()).first->second; 415 Json::Value schemaJson = LoadSchemaDoc(manifestVersion, entry.ManifestType); 416 JsonSchema::PopulateSchema(schemaJson, newSchema); 417 } 418 419 const auto& schema = schemaList.find(entry.ManifestType)->second; 420 Json::Value manifestJson = ManifestYamlNodeToJson(entry.Root); 421 valijson::ValidationResults results; 422 423 if (!JsonSchema::Validate(schema, manifestJson, results)) 424 { 425 errors.emplace_back(ValidationError::MessageContextWithFile(ManifestError::SchemaError, JsonSchema::GetErrorStringFromResults(results), entry.FileName)); 426 } 427 } 428 429 return errors; 430 } 431 432 std::vector<ValidationError> ValidateYamlManifestsSchemaHeader(const std::vector<YamlManifestInfo>& manifestList, const ManifestVer& manifestVersion, bool treatErrorAsWarning) 433 { 434 std::vector<ValidationError> errors; 435 ValidationError::Level errorLevel = treatErrorAsWarning ? ValidationError::Level::Warning : ValidationError::Level::Error; 436 437 // Read the manifest schema header and ensure it exists 438 for (const auto& entry : manifestList) 439 { 440 if (entry.ManifestType == ManifestTypeEnum::Shadow) 441 { 442 // There's no schema for a shadow manifest. 443 continue; 444 } 445 446 auto schemaHeaderErrors = ValidateYamlManifestSchemaHeader(entry, manifestVersion, errorLevel); 447 std::move(schemaHeaderErrors.begin(), schemaHeaderErrors.end(), std::inserter(errors, errors.end())); 448 } 449 450 return errors; 451 } 452 }