ConfigurationSetParser.cpp (24742B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "ConfigurationSetParser.h" 5 #include "ParsingMacros.h" 6 #include "ArgumentValidation.h" 7 8 #include <AppInstallerErrors.h> 9 #include <AppInstallerLogging.h> 10 #include <AppInstallerStrings.h> 11 #include <AppInstallerVersions.h> 12 13 #include "ConfigurationSetUtilities.h" 14 #include "ConfigurationSetParserError.h" 15 #include "ConfigurationSetParser_0_1.h" 16 #include "ConfigurationSetParser_0_2.h" 17 #include "ConfigurationSetParser_0_3.h" 18 19 using namespace AppInstaller::Utility; 20 using namespace AppInstaller::YAML; 21 22 namespace winrt::Microsoft::Management::Configuration::implementation 23 { 24 namespace 25 { 26 struct SchemaVersionAndUri 27 { 28 std::string_view Version; 29 std::wstring_view VersionWide; 30 std::string_view Uri; 31 std::wstring_view UriWide; 32 }; 33 34 #define SCHEMA_VERSION_MAP_ITEM(_version_,_uri_) _version_, TEXT(_version_), _uri_, TEXT(_uri_) 35 36 // Please keep in sorted order with the highest version last. 37 // Duplicate URIs are supported, but duplicate versions are not. The highest version for a URI will be the one mapped to, the lower versions will be aliases. 38 SchemaVersionAndUri SchemaVersionAndUriMap[] = 39 { 40 { SCHEMA_VERSION_MAP_ITEM("0.1", "") }, 41 { SCHEMA_VERSION_MAP_ITEM("0.2", "") }, 42 { SCHEMA_VERSION_MAP_ITEM("0.3", "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/config/document.json") }, 43 }; 44 45 Windows::Foundation::IInspectable GetIInspectableFromNode(const Node& node); 46 47 // Fills the ValueSet from the given node, which is assumed to be a map. 48 void FillValueSetFromMap(const Node& mapNode, const Windows::Foundation::Collections::ValueSet& valueSet) 49 { 50 for (const auto& mapItem : mapNode.Mapping()) 51 { 52 // Insert returns true if it replaces an existing key, and that indicates an invalid map. 53 THROW_HR_IF(WINGET_CONFIG_ERROR_INVALID_CONFIGURATION_FILE, valueSet.Insert(mapItem.first.as<std::wstring>(), GetIInspectableFromNode(mapItem.second))); 54 } 55 } 56 57 // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar. 58 Windows::Foundation::IInspectable GetPropertyValueFromScalar(const Node& node) 59 { 60 ::winrt::Windows::Foundation::IInspectable result; 61 62 switch (node.GetTagType()) 63 { 64 case Node::TagType::Null: 65 return Windows::Foundation::PropertyValue::CreateEmpty(); 66 case Node::TagType::Bool: 67 return Windows::Foundation::PropertyValue::CreateBoolean(node.as<bool>()); 68 case Node::TagType::Str: 69 return Windows::Foundation::PropertyValue::CreateString(node.as<std::wstring>()); 70 case Node::TagType::Int: 71 return Windows::Foundation::PropertyValue::CreateInt64(node.as<int64_t>()); 72 case Node::TagType::Float: 73 THROW_HR(E_NOTIMPL); 74 case Node::TagType::Timestamp: 75 THROW_HR(E_NOTIMPL); 76 default: 77 THROW_HR(E_UNEXPECTED); 78 } 79 } 80 81 // Returns the appropriate IPropertyValue for the given node, which is assumed to be a scalar. 82 Windows::Foundation::IInspectable GetPropertyValueFromSequence(const Node& sequenceNode) 83 { 84 Windows::Foundation::Collections::ValueSet result; 85 size_t index = 0; 86 87 for (const Node& sequenceItem : sequenceNode.Sequence()) 88 { 89 std::wostringstream strstr; 90 strstr << index++; 91 result.Insert(strstr.str(), GetIInspectableFromNode(sequenceItem)); 92 } 93 94 result.Insert(L"treatAsArray", Windows::Foundation::PropertyValue::CreateBoolean(true)); 95 return result; 96 } 97 98 // Returns the appropriate IInspectable for the given node. 99 Windows::Foundation::IInspectable GetIInspectableFromNode(const Node& node) 100 { 101 ::winrt::Windows::Foundation::IInspectable result; 102 103 switch (node.GetType()) 104 { 105 case Node::Type::Invalid: 106 case Node::Type::None: 107 // Leave value as null 108 break; 109 case Node::Type::Scalar: 110 result = GetPropertyValueFromScalar(node); 111 break; 112 case Node::Type::Sequence: 113 result = GetPropertyValueFromSequence(node); 114 break; 115 case Node::Type::Mapping: 116 { 117 Windows::Foundation::Collections::ValueSet subset; 118 FillValueSetFromMap(node, subset); 119 result = std::move(subset); 120 } 121 break; 122 default: 123 THROW_HR(E_UNEXPECTED); 124 } 125 126 return result; 127 } 128 129 // Contains the qualified resource name information. 130 struct QualifiedResourceName 131 { 132 QualifiedResourceName(hstring input) 133 { 134 std::wstring_view inputView = input; 135 size_t pos = inputView.find('/'); 136 137 if (pos != std::wstring_view::npos) 138 { 139 Module = inputView.substr(0, pos); 140 Resource = inputView.substr(pos + 1); 141 } 142 else 143 { 144 Resource = input; 145 } 146 } 147 148 hstring Module; 149 hstring Resource; 150 }; 151 } 152 153 std::unique_ptr<ConfigurationSetParser> ConfigurationSetParser::Create(std::string_view input) 154 { 155 AICLI_LOG_LARGE_STRING(Config, Verbose, << "Parsing configuration set:", input); 156 157 Node document; 158 std::string documentError; 159 Mark documentErrorMark; 160 161 try 162 { 163 document = Load(input); 164 } 165 catch (const Exception& exc) 166 { 167 documentError = exc.what(); 168 documentErrorMark = exc.GetMark(); 169 } 170 CATCH_LOG(); 171 172 if (!document.IsMap()) 173 { 174 AICLI_LOG(Config, Error, << "Invalid YAML: " << documentError << " at [line " << documentErrorMark.line << ", col " << documentErrorMark.column << "]"); 175 return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_YAML, documentError, documentErrorMark); 176 } 177 178 // The schema version for parsing the rest of the document 179 std::string schemaUriString; 180 std::string schemaVersionString; 181 182 Node& schemaNode = document[GetConfigurationFieldName(ConfigurationField::Schema)]; 183 if (schemaNode.IsScalar()) 184 { 185 schemaUriString = schemaNode.as<std::string>(); 186 schemaVersionString = GetSchemaVersionForUri(schemaUriString); 187 AICLI_LOG(Config, Verbose, << "Configuration schema `" << schemaNode.as<std::string>() << "` mapped to version `" << schemaVersionString << "`."); 188 } 189 190 // If we recognize the schema, use that version. 191 // If we didn't recognize it, try using the older format. 192 if (schemaVersionString.empty()) 193 { 194 std::unique_ptr<ConfigurationSetParser> oldFormatError = GetSchemaVersionFromOldFormat(document, schemaVersionString); 195 196 // We have no schema version at all... 197 if (oldFormatError) 198 { 199 // If the schema was provided and we didn't recognize it, make that the error. 200 if (schemaNode.IsScalar()) 201 { 202 AICLI_LOG(Config, Error, << "Unknown configuration schema: " << schemaUriString); 203 return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION, GetConfigurationFieldName(ConfigurationField::Schema), schemaUriString); 204 } 205 else 206 { 207 // Otherwise, this is an older format file (or neither). The proper error came back from that function. 208 return oldFormatError; 209 } 210 } 211 } 212 213 // Create the parser based on the version selected 214 auto result = CreateForSchemaVersion(std::move(schemaVersionString)); 215 result->SetDocument(std::move(document)); 216 return result; 217 } 218 219 std::unique_ptr<ConfigurationSetParser> ConfigurationSetParser::CreateForSchemaVersion(std::string input) 220 { 221 SemanticVersion schemaVersion(std::move(input)); 222 223 // TODO: Consider having the version/uri/type information all together in the future 224 if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 1) 225 { 226 return std::make_unique<ConfigurationSetParser_0_1>(); 227 } 228 else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 2) 229 { 230 return std::make_unique<ConfigurationSetParser_0_2>(); 231 } 232 else if (schemaVersion.PartAt(0).Integer == 0 && schemaVersion.PartAt(1).Integer == 3) 233 { 234 return std::make_unique<ConfigurationSetParser_0_3>(); 235 } 236 237 AICLI_LOG(Config, Error, << "Unknown configuration version: " << schemaVersion.ToString()); 238 return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_UNKNOWN_CONFIGURATION_FILE_VERSION, GetConfigurationFieldName(ConfigurationField::ConfigurationVersion), schemaVersion.ToString()); 239 } 240 241 bool ConfigurationSetParser::IsRecognizedSchemaVersion(hstring value) try 242 { 243 SemanticVersion schemaVersion(ConvertToUTF8(value)); 244 245 for (const auto& item : SchemaVersionAndUriMap) 246 { 247 if (schemaVersion == SemanticVersion{ std::string{ item.Version } }) 248 { 249 return true; 250 } 251 } 252 253 return false; 254 } 255 catch (...) { LOG_CAUGHT_EXCEPTION(); return false; } 256 257 bool ConfigurationSetParser::IsRecognizedSchemaUri(const Windows::Foundation::Uri& value) 258 { 259 return !GetSchemaVersionForUri(value).empty(); 260 } 261 262 Windows::Foundation::Uri ConfigurationSetParser::GetSchemaUriForVersion(hstring value) 263 { 264 for (const auto& item : SchemaVersionAndUriMap) 265 { 266 if (value == item.VersionWide) 267 { 268 return item.Uri.empty() ? nullptr : Windows::Foundation::Uri{ item.UriWide }; 269 } 270 } 271 272 return nullptr; 273 } 274 275 hstring ConfigurationSetParser::GetSchemaVersionForUri(Windows::Foundation::Uri value) 276 { 277 // Do a reverse search in order to give the highest version back for a given URI. 278 auto itr = std::rbegin(SchemaVersionAndUriMap); 279 auto end = std::rend(SchemaVersionAndUriMap); 280 for (; itr != end; ++itr) 281 { 282 const auto& item = *itr; 283 if (!item.Uri.empty()) 284 { 285 Windows::Foundation::Uri uri{ item.UriWide }; 286 if (value.Equals(uri)) 287 { 288 return hstring{ item.VersionWide }; 289 } 290 } 291 } 292 293 return {}; 294 } 295 296 std::string ConfigurationSetParser::GetSchemaVersionForUri(std::string_view value) 297 { 298 // Do a reverse search in order to give the highest version back for a given URI. 299 auto itr = std::rbegin(SchemaVersionAndUriMap); 300 auto end = std::rend(SchemaVersionAndUriMap); 301 for (; itr != end; ++itr) 302 { 303 const auto& item = *itr; 304 if (!item.Uri.empty()) 305 { 306 if (item.Uri == value) 307 { 308 return std::string{ item.Version }; 309 } 310 } 311 } 312 313 return {}; 314 } 315 316 std::pair<hstring, Windows::Foundation::Uri> ConfigurationSetParser::LatestVersion() 317 { 318 auto latest = std::rbegin(SchemaVersionAndUriMap); 319 return { hstring{ latest->VersionWide }, Windows::Foundation::Uri{ latest->UriWide } }; 320 } 321 322 Windows::Foundation::Collections::ValueSet ConfigurationSetParser::ParseValueSet(std::string_view input) 323 { 324 Windows::Foundation::Collections::ValueSet result; 325 FillValueSetFromMap(Load(input), result); 326 return result; 327 } 328 329 std::vector<hstring> ConfigurationSetParser::ParseStringArray(std::string_view input) 330 { 331 std::vector<hstring> result; 332 ParseSequence(Load(input), "string_array", Node::Type::Scalar, [&](const AppInstaller::YAML::Node& item) 333 { 334 result.emplace_back(item.as<std::wstring>()); 335 }); 336 return result; 337 } 338 339 void ConfigurationSetParser::SetError(hresult result, std::string_view field, std::string_view value, uint32_t line, uint32_t column) 340 { 341 AICLI_LOG(Config, Error, << "ConfigurationSetParser error: " << AppInstaller::Logging::SetHRFormat << result << " for " << field << " with value `" << value << "` at [line " << line << ", col " << column << "]"); 342 m_result = result; 343 m_field = ConvertToUTF16(field); 344 m_value = ConvertToUTF16(value); 345 m_line = line; 346 m_column = column; 347 } 348 349 void ConfigurationSetParser::SetError(hresult result, std::string_view field, const Mark& mark, std::string_view value) 350 { 351 SetError(result, field, value, static_cast<uint32_t>(mark.line), static_cast<uint32_t>(mark.column)); 352 } 353 354 const Node& ConfigurationSetParser::GetAndEnsureField(const Node& parent, ConfigurationField field, bool required, std::optional<Node::Type> type) 355 { 356 const Node& fieldNode = parent[GetConfigurationFieldName(field)]; 357 358 if (fieldNode) 359 { 360 if (type && fieldNode.GetType() != type.value()) 361 { 362 SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetConfigurationFieldName(field), fieldNode.Mark()); 363 } 364 } 365 else if (required) 366 { 367 SetError(WINGET_CONFIG_ERROR_MISSING_FIELD, GetConfigurationFieldName(field)); 368 } 369 370 return fieldNode; 371 } 372 373 void ConfigurationSetParser::EnsureFieldAbsent(const Node& parent, ConfigurationField field) 374 { 375 const Node& fieldNode = parent[GetConfigurationFieldName(field)]; 376 377 if (fieldNode) 378 { 379 SetError(WINGET_CONFIG_ERROR_INVALID_FIELD_VALUE, GetConfigurationFieldName(field), fieldNode.Mark(), fieldNode.as<std::string>()); 380 } 381 } 382 383 void ConfigurationSetParser::ParseValueSet(const Node& node, ConfigurationField field, bool required, const Windows::Foundation::Collections::ValueSet& valueSet) 384 { 385 const Node& mapNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Mapping)); 386 387 if (mapNode) 388 { 389 FillValueSetFromMap(mapNode, valueSet); 390 } 391 } 392 393 void ConfigurationSetParser::ParseMapping(const AppInstaller::YAML::Node& node, ConfigurationField field, bool required, AppInstaller::YAML::Node::Type elementType, std::function<void(std::string, const AppInstaller::YAML::Node&)> operation) 394 { 395 const Node& mapNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Mapping)); 396 if (!mapNode) 397 { 398 return; 399 } 400 401 std::ostringstream strstr; 402 strstr << GetConfigurationFieldName(field); 403 size_t index = 0; 404 405 for (const auto& mapItem : mapNode.Mapping()) 406 { 407 std::string name = mapItem.first.as<std::string>(); 408 if (name.empty()) 409 { 410 strstr << '[' << index << ']'; 411 FIELD_VALUE_ERROR(strstr.str(), name, mapItem.first.Mark()); 412 } 413 414 if (mapItem.second.GetType() != elementType) 415 { 416 strstr << '[' << index << ']'; 417 FIELD_TYPE_ERROR(strstr.str(), mapItem.second.Mark()); 418 } 419 index++; 420 421 CHECK_ERROR(operation(std::move(name), mapItem.second)); 422 } 423 } 424 425 void ConfigurationSetParser::ParseSequence(const AppInstaller::YAML::Node& node, ConfigurationField field, bool required, std::optional<Node::Type> elementType, std::function<void(const AppInstaller::YAML::Node&)> operation) 426 { 427 const Node& sequenceNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Sequence)); 428 if (!sequenceNode) 429 { 430 return; 431 } 432 433 ParseSequence(sequenceNode, GetConfigurationFieldName(field), elementType, operation); 434 } 435 436 void ConfigurationSetParser::ParseSequence(const AppInstaller::YAML::Node& node, std::string_view nameForErrors, std::optional<Node::Type> elementType, std::function<void(const AppInstaller::YAML::Node&)> operation) 437 { 438 std::ostringstream strstr; 439 strstr << nameForErrors; 440 size_t index = 0; 441 442 for (const Node& item : node.Sequence()) 443 { 444 if (elementType && item.GetType() != elementType.value()) 445 { 446 strstr << '[' << index << ']'; 447 FIELD_TYPE_ERROR(strstr.str(), item.Mark()); 448 } 449 index++; 450 451 CHECK_ERROR(operation(item)); 452 } 453 } 454 455 std::unique_ptr<ConfigurationSetParser> ConfigurationSetParser::GetSchemaVersionFromOldFormat(AppInstaller::YAML::Node& document, std::string& schemaVersionString) 456 { 457 Node& propertiesNode = document[GetConfigurationFieldName(ConfigurationField::Properties)]; 458 if (!propertiesNode) 459 { 460 AICLI_LOG(Config, Error, << "No properties"); 461 // Even though this is for the "older" format, if there is no properties entry then give an error for the newer format since this is probably neither. 462 return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_MISSING_FIELD, GetConfigurationFieldName(ConfigurationField::Schema)); 463 } 464 else if (!propertiesNode.IsMap()) 465 { 466 AICLI_LOG(Config, Error, << "Invalid properties type"); 467 return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetConfigurationFieldName(ConfigurationField::Properties), propertiesNode.Mark()); 468 } 469 470 Node& versionNode = propertiesNode[GetConfigurationFieldName(ConfigurationField::ConfigurationVersion)]; 471 if (!versionNode) 472 { 473 AICLI_LOG(Config, Error, << "No configuration version"); 474 return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_MISSING_FIELD, GetConfigurationFieldName(ConfigurationField::ConfigurationVersion)); 475 } 476 else if (!versionNode.IsScalar()) 477 { 478 AICLI_LOG(Config, Error, << "Invalid configuration version type"); 479 return std::make_unique<ConfigurationSetParserError>(WINGET_CONFIG_ERROR_INVALID_FIELD_TYPE, GetConfigurationFieldName(ConfigurationField::ConfigurationVersion), versionNode.Mark()); 480 } 481 482 schemaVersionString = versionNode.as<std::string>(); 483 return {}; 484 } 485 486 void ConfigurationSetParser::GetStringValueForUnit(const Node& node, ConfigurationField field, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(const hstring& value)) 487 { 488 const Node& valueNode = CHECK_ERROR(GetAndEnsureField(node, field, required, Node::Type::Scalar)); 489 490 if (valueNode) 491 { 492 hstring value{ valueNode.as<std::wstring>() }; 493 FIELD_MISSING_ERROR_IF(value.empty() && required, GetConfigurationFieldName(field)); 494 495 (unit->*propertyFunction)(std::move(value)); 496 } 497 } 498 499 void ConfigurationSetParser::GetStringArrayForUnit(const Node& node, ConfigurationField field, bool required, ConfigurationUnit* unit, void(ConfigurationUnit::* propertyFunction)(std::vector<hstring>&& value)) 500 { 501 std::vector<hstring> arrayValue; 502 CHECK_ERROR(ParseSequence(node, field, required, Node::Type::Scalar, [&](const AppInstaller::YAML::Node& item) 503 { 504 arrayValue.emplace_back(item.as<std::wstring>()); 505 })); 506 507 if (!arrayValue.empty()) 508 { 509 (unit->*propertyFunction)(std::move(arrayValue)); 510 } 511 } 512 513 void ConfigurationSetParser::ValidateType(ConfigurationUnit* unit, const Node& unitNode, ConfigurationField typeField, bool moveModuleNameToMetadata, bool moduleNameRequiredInType) 514 { 515 QualifiedResourceName qualifiedName{ unit->Type() }; 516 517 const Node& typeNode = CHECK_ERROR(GetAndEnsureField(unitNode, typeField, true, Node::Type::Scalar)); 518 FIELD_VALUE_ERROR_IF(qualifiedName.Resource.empty(), GetConfigurationFieldName(typeField), ConvertToUTF8(unit->Type()), typeNode.Mark()); 519 520 if (!qualifiedName.Module.empty()) 521 { 522 // If the module is provided in both the resource name and the directives, ensure that it matches 523 hstring moduleDirectiveFieldName = GetConfigurationFieldNameHString(ConfigurationField::ModuleDirective); 524 auto moduleDirective = unit->Metadata().TryLookup(moduleDirectiveFieldName); 525 if (moduleDirective) 526 { 527 auto moduleProperty = moduleDirective.try_as<Windows::Foundation::IPropertyValue>(); 528 FIELD_TYPE_ERROR_IF(!moduleProperty, GetConfigurationFieldName(ConfigurationField::ModuleDirective), unitNode.Mark()); 529 FIELD_TYPE_ERROR_IF(moduleProperty.Type() != Windows::Foundation::PropertyType::String, GetConfigurationFieldName(ConfigurationField::ModuleDirective), unitNode.Mark()); 530 hstring moduleValue = moduleProperty.GetString(); 531 FIELD_VALUE_ERROR_IF(qualifiedName.Module != moduleValue, GetConfigurationFieldName(ConfigurationField::ModuleDirective), ConvertToUTF8(moduleValue), unitNode.Mark()); 532 } 533 else if (moveModuleNameToMetadata) 534 { 535 unit->Metadata().Insert(moduleDirectiveFieldName, Windows::Foundation::PropertyValue::CreateString(qualifiedName.Module)); 536 } 537 538 if (moveModuleNameToMetadata) 539 { 540 // Set the unit name to be just the resource portion 541 unit->Type(qualifiedName.Resource); 542 } 543 } 544 else if (moduleNameRequiredInType) 545 { 546 FIELD_VALUE_ERROR(GetConfigurationFieldName(typeField), ConvertToUTF8(unit->Type()), typeNode.Mark()); 547 } 548 } 549 550 void ConfigurationSetParser::ParseObject(const Node& node, ConfigurationField fieldForErrors, Windows::Foundation::PropertyType type, Windows::Foundation::IInspectable& result) 551 { 552 try 553 { 554 Windows::Foundation::IInspectable object = GetIInspectableFromNode(node); 555 FIELD_VALUE_ERROR_IF(!IsValidObjectType(object, type), GetConfigurationFieldName(fieldForErrors), node.as<std::string>(), node.Mark()); 556 result = std::move(object); 557 } 558 catch (...) 559 { 560 LOG_CAUGHT_EXCEPTION(); 561 FIELD_VALUE_ERROR(GetConfigurationFieldName(fieldForErrors), node.as<std::string>(), node.Mark()); 562 } 563 } 564 565 void ConfigurationSetParser::ExtractSecurityContext(implementation::ConfigurationUnit* unit, SecurityContext defaultContext) 566 { 567 THROW_HR_IF_NULL(E_POINTER, unit); 568 569 ExtractSecurityContext(unit->Metadata(), unit->EnvironmentInternal(), defaultContext); 570 } 571 572 void ConfigurationSetParser::ExtractSecurityContext(Windows::Foundation::Collections::ValueSet metadata, implementation::ConfigurationEnvironment& environment, SecurityContext defaultContext) 573 { 574 SecurityContext computedContext = defaultContext; 575 576 auto securityContext = TryLookupProperty(metadata, ConfigurationField::SecurityContextMetadata, Windows::Foundation::PropertyType::String); 577 if (securityContext) 578 { 579 TryParseSecurityContext(securityContext.GetString(), computedContext); 580 metadata.Remove(GetConfigurationFieldNameHString(ConfigurationField::SecurityContextMetadata)); 581 } 582 583 environment.Context(computedContext); 584 } 585 }