Runtime.cpp (24063B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include <binver/version.h> 5 #include "Public/AppInstallerLogging.h" 6 #include "Public/AppInstallerRuntime.h" 7 #include "Public/AppInstallerStrings.h" 8 #include "Public/winget/UserSettings.h" 9 #include "Public/winget/Registry.h" 10 #include <winget/Filesystem.h> 11 12 13 #define WINGET_DEFAULT_LOG_DIRECTORY "DiagOutputDir" 14 15 namespace AppInstaller::Runtime 16 { 17 using namespace Utility; 18 using namespace Settings; 19 using namespace Filesystem; 20 21 namespace 22 { 23 using namespace std::string_view_literals; 24 constexpr std::string_view s_DefaultTempDirectory = "WinGet"sv; 25 constexpr std::string_view s_SecureSettings_Base = "Microsoft\\WinGet"sv; 26 constexpr std::string_view s_SecureSettings_UserRelative = "settings"sv; 27 constexpr std::string_view s_SecureSettings_Relative_Unpackaged = "win"sv; 28 constexpr std::string_view s_PortablePackageUserRoot_Base = "Microsoft"sv; 29 constexpr std::string_view s_PortablePackageRoot = "WinGet"sv; 30 constexpr std::string_view s_PortablePackagesDirectory = "Packages"sv; 31 constexpr std::string_view s_LinksDirectory = "Links"sv; 32 constexpr std::string_view s_FontsInstallDirectory = "Microsoft\\Windows\\Fonts"sv; 33 constexpr std::string_view s_ConfigurationModulesDirectory = "Configuration\\Modules"sv; 34 // Use production CLSIDs as a surrogate for repository location. 35 #if USE_PROD_CLSIDS 36 constexpr std::string_view s_ImageAssetsDirectoryRelative = "Assets\\WinGet"sv; 37 #else 38 constexpr std::string_view s_ImageAssetsDirectoryRelative = "Images"sv; 39 #endif 40 constexpr std::string_view s_CheckpointsDirectory = "Checkpoints"sv; 41 constexpr std::string_view s_DevModeSubkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock"sv; 42 constexpr std::string_view s_AllowDevelopmentWithoutDevLicense = "AllowDevelopmentWithoutDevLicense"sv; 43 #ifndef WINGET_DISABLE_FOR_FUZZING 44 constexpr std::string_view s_SecureSettings_Relative_Packaged = "pkg"sv; 45 #endif 46 constexpr std::string_view s_RuntimePath_Unpackaged_DefaultState = "defaultState"sv; 47 48 constexpr std::string_view s_UserProfileEnvironmentVariable = "%USERPROFILE%"; 49 constexpr std::string_view s_LocalAppDataEnvironmentVariable = "%LOCALAPPDATA%"; 50 constexpr std::string_view s_WindowsApps_Base = "Microsoft\\WindowsApps"sv; 51 constexpr std::string_view s_WinGetDev_Exe = "wingetdev.exe"; 52 constexpr std::string_view s_WinGet_Exe = "winget.exe"; 53 constexpr std::string_view s_WinGetMCPDev_Exe = "WindowsPackageManagerMCPServerDev.exe"; 54 constexpr std::string_view s_WinGetMCP_Exe = "WindowsPackageManagerMCPServer.exe"; 55 56 static std::optional<std::string> s_runtimePathStateName; 57 static wil::srwlock s_runtimePathStateNameLock; 58 59 // Gets the path to the root of the package containing the current process. 60 std::filesystem::path GetPackagePath() 61 { 62 wchar_t packageFullName[PACKAGE_FULL_NAME_MAX_LENGTH + 1]; 63 UINT32 nameLength = ARRAYSIZE(packageFullName); 64 THROW_IF_WIN32_ERROR(GetPackageFullName(GetCurrentProcess(), &nameLength, packageFullName)); 65 66 UINT32 pathLength = 0; 67 LONG result = GetPackagePathByFullName(packageFullName, &pathLength, nullptr); 68 THROW_HR_IF(HRESULT_FROM_WIN32(result), result != ERROR_INSUFFICIENT_BUFFER); 69 70 std::unique_ptr<wchar_t[]> buffer = std::make_unique<wchar_t[]>(pathLength); 71 THROW_IF_WIN32_ERROR(GetPackagePathByFullName(packageFullName, &pathLength, buffer.get())); 72 73 return { buffer.get() }; 74 } 75 76 // Gets the path to the directory containing the currently executing binary file. 77 std::filesystem::path GetBinaryDirectoryPath() 78 { 79 HMODULE moduleHandle = NULL; 80 THROW_IF_WIN32_BOOL_FALSE(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, 81 (LPCWSTR)&GetBinaryDirectoryPath, &moduleHandle)); 82 83 // Get the path for this module. 84 wil::unique_process_heap_string binaryPath; 85 THROW_IF_FAILED(wil::GetModuleFileNameW(moduleHandle, binaryPath)); 86 87 std::filesystem::path resultFilePath{ binaryPath.get() }; 88 return resultFilePath.parent_path(); 89 } 90 91 std::unique_ptr<byte[]> GetPACKAGE_ID() 92 { 93 UINT32 bufferLength = 0; 94 LONG gcpiResult = GetCurrentPackageId(&bufferLength, nullptr); 95 THROW_HR_IF(E_UNEXPECTED, gcpiResult != ERROR_INSUFFICIENT_BUFFER); 96 97 std::unique_ptr<byte[]> buffer = std::make_unique<byte[]>(bufferLength); 98 99 gcpiResult = GetCurrentPackageId(&bufferLength, buffer.get()); 100 if (FAILED_WIN32_LOG(gcpiResult)) 101 { 102 return {}; 103 } 104 105 return buffer; 106 } 107 108 // Gets the package name; only succeeds if running in a packaged context. 109 std::string GetPackageName() 110 { 111 std::unique_ptr<byte[]> buffer = GetPACKAGE_ID(); 112 if (!buffer) 113 { 114 return {}; 115 } 116 117 PACKAGE_ID* packageId = reinterpret_cast<PACKAGE_ID*>(buffer.get()); 118 return Utility::ConvertToUTF8(packageId->name); 119 } 120 121 #ifndef AICLI_DISABLE_TEST_HOOKS 122 static std::map<PathName, PathDetails> s_Path_TestHook_Overrides; 123 #endif 124 125 // Gets the user's temp path 126 std::filesystem::path GetPathToUserTemp(bool forDisplay) 127 { 128 if (forDisplay && Settings::User().Get<Setting::AnonymizePathForDisplay>()) 129 { 130 return "%TEMP%"; 131 } 132 else 133 { 134 wchar_t tempPath[MAX_PATH + 1]; 135 DWORD tempChars = GetTempPathW(ARRAYSIZE(tempPath), tempPath); 136 THROW_LAST_ERROR_IF(!tempChars); 137 THROW_HR_IF(E_UNEXPECTED, tempChars > ARRAYSIZE(tempPath)); 138 return { std::wstring_view{ tempPath, static_cast<size_t>(tempChars) } }; 139 } 140 } 141 142 // Gets the current user's SID for use in paths. 143 std::filesystem::path GetUserSID() 144 { 145 auto userToken = wil::get_token_information<TOKEN_USER>(); 146 147 wil::unique_hlocal_string sidString; 148 THROW_IF_WIN32_BOOL_FALSE(ConvertSidToStringSidW(userToken->User.Sid, &sidString)); 149 return { sidString.get() }; 150 } 151 152 std::string GetRuntimePathStateName() 153 { 154 std::string result; 155 auto lock = s_runtimePathStateNameLock.lock_shared(); 156 157 if (s_runtimePathStateName.has_value()) 158 { 159 result = s_runtimePathStateName.value(); 160 } 161 162 if (Utility::IsEmptyOrWhitespace(result)) 163 { 164 result = s_RuntimePath_Unpackaged_DefaultState; 165 } 166 167 return result; 168 } 169 } 170 171 void SetRuntimePathStateName(std::string name) 172 { 173 auto suitablePathPart = MakeSuitablePathPart(name); 174 auto lock = s_runtimePathStateNameLock.lock_exclusive(); 175 s_runtimePathStateName.emplace(std::move(suitablePathPart)); 176 } 177 178 // Contains all of the paths that are common between the runtime contexts. 179 PathDetails GetPathDetailsCommon(PathName path, bool forDisplay) 180 { 181 PathDetails result; 182 // We should not create directories by default when they are retrieved for display purposes. 183 result.Create = !forDisplay; 184 185 bool mayBeInProfilePath = false; 186 187 switch (path) 188 { 189 case PathName::UserProfile: 190 result.Path = (forDisplay && Settings::User().Get<Setting::AnonymizePathForDisplay>()) ? s_UserProfileEnvironmentVariable : GetKnownFolderPath(FOLDERID_Profile); 191 result.Create = false; 192 break; 193 case PathName::PortablePackageUserRoot: 194 result.Path = Settings::User().Get<Setting::PortablePackageUserRoot>(); 195 if (result.Path.empty()) 196 { 197 result.Path = GetKnownFolderPath(FOLDERID_LocalAppData); 198 result.Path /= s_PortablePackageUserRoot_Base; 199 result.Path /= s_PortablePackageRoot; 200 result.Path /= s_PortablePackagesDirectory; 201 } 202 mayBeInProfilePath = true; 203 break; 204 case PathName::PortablePackageMachineRoot: 205 result.Path = Settings::User().Get<Setting::PortablePackageMachineRoot>(); 206 if (result.Path.empty()) 207 { 208 result.Path = GetKnownFolderPath(FOLDERID_ProgramFiles); 209 result.Path /= s_PortablePackageRoot; 210 result.Path /= s_PortablePackagesDirectory; 211 } 212 break; 213 case PathName::PortablePackageMachineRootX86: 214 result.Path = Settings::User().Get<Setting::PortablePackageMachineRoot>(); 215 if (result.Path.empty()) 216 { 217 result.Path = GetKnownFolderPath(FOLDERID_ProgramFilesX86); 218 result.Path /= s_PortablePackageRoot; 219 result.Path /= s_PortablePackagesDirectory; 220 } 221 break; 222 case PathName::PortableLinksUserLocation: 223 result.Path = GetKnownFolderPath(FOLDERID_LocalAppData); 224 result.Path /= s_PortablePackageUserRoot_Base; 225 result.Path /= s_PortablePackageRoot; 226 result.Path /= s_LinksDirectory; 227 mayBeInProfilePath = true; 228 break; 229 case PathName::PortableLinksMachineLocation: 230 result.Path = GetKnownFolderPath(FOLDERID_ProgramFiles); 231 result.Path /= s_PortablePackageRoot; 232 result.Path /= s_LinksDirectory; 233 break; 234 case PathName::UserProfileDownloads: 235 result.Path = GetKnownFolderPath(FOLDERID_Downloads); 236 mayBeInProfilePath = true; 237 break; 238 case PathName::FontsUserInstallLocation: 239 result.Path = GetKnownFolderPath(FOLDERID_LocalAppData); 240 result.Path /= s_FontsInstallDirectory; 241 mayBeInProfilePath = true; 242 break; 243 case PathName::FontsMachineInstallLocation: 244 result.Path = GetKnownFolderPath(FOLDERID_Fonts); 245 break; 246 case PathName::ConfigurationModules: 247 result.Path = Settings::User().Get<Setting::ConfigureDefaultModuleRoot>(); 248 if (result.Path.empty()) 249 { 250 result.Path = GetKnownFolderPath(FOLDERID_LocalAppData); 251 result.Path /= s_SecureSettings_Base; 252 result.Path /= s_ConfigurationModulesDirectory; 253 } 254 mayBeInProfilePath = true; 255 break; 256 default: 257 THROW_HR(E_UNEXPECTED); 258 } 259 260 if (mayBeInProfilePath && forDisplay && Settings::User().Get<Setting::AnonymizePathForDisplay>()) 261 { 262 ReplaceProfilePathsWithEnvironmentVariable(result.Path); 263 } 264 265 return result; 266 } 267 268 #ifndef WINGET_DISABLE_FOR_FUZZING 269 PathDetails GetPathDetailsForPackagedContext(PathName path, bool forDisplay) 270 { 271 PathDetails result; 272 // We should not create directories by default when they are retrieved for display purposes. 273 result.Create = !forDisplay; 274 275 auto appStorage = winrt::Windows::Storage::ApplicationData::Current(); 276 bool mayBeInProfilePath = false; 277 278 switch (path) 279 { 280 case PathName::Temp: 281 result.Path = GetPathToUserTemp(forDisplay) / s_DefaultTempDirectory; 282 result.SetOwner(ACEPrincipal::CurrentUser); 283 result.ACL[ACEPrincipal::System] = ACEPermissions::All; 284 result.ACL[ACEPrincipal::Admins] = ACEPermissions::All; 285 break; 286 case PathName::LocalState: 287 case PathName::UserFileSettings: 288 result.Path.assign(appStorage.LocalFolder().Path().c_str()); 289 mayBeInProfilePath = true; 290 break; 291 case PathName::DefaultLogLocation: 292 // To enable UIF collection through Feedback hub, we must put our logs here. 293 result.Path.assign(appStorage.LocalFolder().Path().c_str()); 294 result.Path /= WINGET_DEFAULT_LOG_DIRECTORY; 295 mayBeInProfilePath = true; 296 break; 297 case PathName::StandardSettings: 298 result.Create = false; 299 break; 300 case PathName::SecureSettingsForRead: 301 case PathName::SecureSettingsForWrite: 302 result.Path = GetKnownFolderPath(FOLDERID_ProgramData); 303 result.Path /= s_SecureSettings_Base; 304 result.Path /= GetUserSID(); 305 result.Path /= s_SecureSettings_UserRelative; 306 result.Path /= s_SecureSettings_Relative_Packaged; 307 result.Path /= GetPackageName(); 308 if (path == PathName::SecureSettingsForWrite) 309 { 310 result.SetOwner(ACEPrincipal::Admins); 311 // When running as system, we do not set current user permissions to avoid permission conflicts. 312 if (!IsRunningAsSystem()) 313 { 314 result.ACL[ACEPrincipal::CurrentUser] = ACEPermissions::ReadExecute; 315 } 316 result.ACL[ACEPrincipal::System] = ACEPermissions::All; 317 } 318 else 319 { 320 result.Create = false; 321 } 322 break; 323 case PathName::UserProfile: 324 case PathName::PortablePackageMachineRoot: 325 case PathName::PortablePackageMachineRootX86: 326 case PathName::PortableLinksMachineLocation: 327 case PathName::PortableLinksUserLocation: 328 case PathName::PortablePackageUserRoot: 329 case PathName::UserProfileDownloads: 330 case PathName::FontsUserInstallLocation: 331 case PathName::FontsMachineInstallLocation: 332 case PathName::ConfigurationModules: 333 result = GetPathDetailsCommon(path, forDisplay); 334 break; 335 case PathName::SelfPackageRoot: 336 case PathName::ImageAssets: 337 result.Path = GetPackagePath(); 338 result.Create = false; 339 if (path == PathName::ImageAssets) 340 { 341 result.Path /= s_ImageAssetsDirectoryRelative; 342 } 343 break; 344 case PathName::CheckpointsLocation: 345 result = GetPathDetailsForPackagedContext(PathName::LocalState, forDisplay); 346 result.Path /= s_CheckpointsDirectory; 347 break; 348 case PathName::CLIExecutable: 349 case PathName::MCPExecutable: 350 result.Path = GetKnownFolderPath(FOLDERID_LocalAppData); 351 result.Path /= s_WindowsApps_Base; 352 result.Path /= GetPackageFamilyName(); 353 354 if (path == PathName::CLIExecutable) 355 { 356 #if USE_PROD_CLSIDS 357 result.Path /= s_WinGet_Exe; 358 #else 359 result.Path /= s_WinGetDev_Exe; 360 #endif 361 } 362 else if (path == PathName::MCPExecutable) 363 { 364 #if USE_PROD_CLSIDS 365 result.Path /= s_WinGetMCP_Exe; 366 #else 367 result.Path /= s_WinGetMCPDev_Exe; 368 #endif 369 } 370 371 result.Create = false; 372 mayBeInProfilePath = true; 373 break; 374 default: 375 THROW_HR(E_UNEXPECTED); 376 } 377 378 if (mayBeInProfilePath && forDisplay && Settings::User().Get<Setting::AnonymizePathForDisplay>()) 379 { 380 ReplaceProfilePathsWithEnvironmentVariable(result.Path); 381 } 382 383 return result; 384 } 385 #endif 386 387 PathDetails GetPathDetailsForUnpackagedContext(PathName path, bool forDisplay) 388 { 389 PathDetails result; 390 // We should not create directories by default when they are retrieved for display purposes. 391 result.Create = !forDisplay; 392 bool anonymize = forDisplay && Settings::User().Get<Setting::AnonymizePathForDisplay>(); 393 394 switch (path) 395 { 396 case PathName::Temp: 397 case PathName::DefaultLogLocation: 398 { 399 result.Path = GetPathToUserTemp(forDisplay); 400 result.Path /= s_DefaultTempDirectory; 401 result.Path /= GetRuntimePathStateName(); 402 if (path == PathName::Temp) 403 { 404 result.SetOwner(ACEPrincipal::CurrentUser); 405 result.ACL[ACEPrincipal::System] = ACEPermissions::All; 406 result.ACL[ACEPrincipal::Admins] = ACEPermissions::All; 407 } 408 } 409 break; 410 case PathName::LocalState: 411 result = Filesystem::GetPathDetailsFor(Filesystem::PathName::UnpackagedLocalStateRoot, anonymize); 412 result.Create = !forDisplay; 413 result.Path /= GetRuntimePathStateName(); 414 break; 415 case PathName::StandardSettings: 416 case PathName::UserFileSettings: 417 result = Filesystem::GetPathDetailsFor(Filesystem::PathName::UnpackagedSettingsRoot, anonymize); 418 result.Create = !forDisplay; 419 result.Path /= GetRuntimePathStateName(); 420 break; 421 case PathName::SecureSettingsForRead: 422 case PathName::SecureSettingsForWrite: 423 result.Path = GetKnownFolderPath(FOLDERID_ProgramData); 424 result.Path /= s_SecureSettings_Base; 425 result.Path /= GetUserSID(); 426 result.Path /= s_SecureSettings_UserRelative; 427 result.Path /= s_SecureSettings_Relative_Unpackaged; 428 result.Path /= GetRuntimePathStateName(); 429 if (path == PathName::SecureSettingsForWrite) 430 { 431 result.SetOwner(ACEPrincipal::Admins); 432 // When running as system, we do not set current user permissions to avoid permission conflicts. 433 if (!IsRunningAsSystem()) 434 { 435 result.ACL[ACEPrincipal::CurrentUser] = ACEPermissions::ReadExecute; 436 } 437 result.ACL[ACEPrincipal::System] = ACEPermissions::All; 438 } 439 else 440 { 441 result.Create = false; 442 } 443 break; 444 case PathName::UserProfile: 445 case PathName::PortablePackageMachineRoot: 446 case PathName::PortablePackageMachineRootX86: 447 case PathName::PortableLinksMachineLocation: 448 case PathName::PortableLinksUserLocation: 449 case PathName::PortablePackageUserRoot: 450 case PathName::UserProfileDownloads: 451 case PathName::FontsUserInstallLocation: 452 case PathName::FontsMachineInstallLocation: 453 case PathName::ConfigurationModules: 454 result = GetPathDetailsCommon(path, forDisplay); 455 break; 456 case PathName::SelfPackageRoot: 457 case PathName::CLIExecutable: 458 case PathName::MCPExecutable: 459 case PathName::ImageAssets: 460 result.Path = GetBinaryDirectoryPath(); 461 result.Create = false; 462 if (path == PathName::CLIExecutable) 463 { 464 result.Path /= s_WinGet_Exe; 465 } 466 else if (path == PathName::MCPExecutable) 467 { 468 result.Path /= s_WinGetMCP_Exe; 469 } 470 else if (path == PathName::ImageAssets) 471 { 472 result.Path /= s_ImageAssetsDirectoryRelative; 473 if (!std::filesystem::is_directory(result.Path)) 474 { 475 result.Path.clear(); 476 } 477 } 478 break; 479 case PathName::CheckpointsLocation: 480 result = GetPathDetailsForUnpackagedContext(PathName::LocalState, forDisplay); 481 result.Path /= s_CheckpointsDirectory; 482 break; 483 default: 484 THROW_HR(E_UNEXPECTED); 485 } 486 487 return result; 488 } 489 490 PathDetails GetPathDetailsFor(PathName path, bool forDisplay) 491 { 492 PathDetails result; 493 494 #ifndef WINGET_DISABLE_FOR_FUZZING 495 if (IsRunningInPackagedContext()) 496 { 497 result = GetPathDetailsForPackagedContext(path, forDisplay); 498 } 499 else 500 #endif 501 { 502 result = GetPathDetailsForUnpackagedContext(path, forDisplay); 503 } 504 505 #ifndef AICLI_DISABLE_TEST_HOOKS 506 // Override the value after letting the normal code path run 507 auto itr = s_Path_TestHook_Overrides.find(path); 508 if (itr != s_Path_TestHook_Overrides.end()) 509 { 510 result = itr->second; 511 } 512 #endif 513 514 return result; 515 } 516 517 // Try to replace LOCALAPPDATA first as it is the likely location, fall back to trying USERPROFILE. 518 void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path) 519 { 520 if (!ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_LocalAppData), s_LocalAppDataEnvironmentVariable)) 521 { 522 ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_Profile), s_UserProfileEnvironmentVariable); 523 } 524 } 525 526 std::filesystem::path GetNewTempFilePath() 527 { 528 GUID guid; 529 THROW_IF_FAILED(CoCreateGuid(&guid)); 530 WCHAR tempFileName[256]; 531 THROW_HR_IF(E_UNEXPECTED, StringFromGUID2(guid, tempFileName, ARRAYSIZE(tempFileName)) == 0); 532 auto tempFilePath = Runtime::GetPathTo(Runtime::PathName::Temp); 533 tempFilePath /= tempFileName; 534 535 return tempFilePath; 536 } 537 538 // Determines whether developer mode is enabled. 539 // Does not account for the group policy value which takes precedence over this registry value. 540 bool IsDevModeEnabled() 541 { 542 const auto& devModeSubKey = Registry::Key::OpenIfExists(HKEY_LOCAL_MACHINE, s_DevModeSubkey, 0, KEY_READ|KEY_WOW64_64KEY); 543 const auto& devModeEnabled = devModeSubKey[s_AllowDevelopmentWithoutDevLicense]; 544 if (devModeEnabled.has_value()) 545 { 546 return devModeEnabled->GetValue<Registry::Value::Type::DWord>() == 1; 547 } 548 else 549 { 550 return false; 551 } 552 } 553 554 // Using "standard" user agent format 555 // Keeping `winget-cli` for historical reasons 556 Utility::LocIndString GetDefaultUserAgent() 557 { 558 std::ostringstream strstr; 559 strstr << 560 "winget-cli" << 561 " WindowsPackageManager/" << GetClientVersion() << 562 " DesktopAppInstaller/" << GetPackageVersion(); 563 return Utility::LocIndString{ strstr.str() }; 564 } 565 566 Utility::LocIndString GetUserAgent(std::string_view caller) 567 { 568 std::ostringstream strstr; 569 strstr << 570 caller << 571 " WindowsPackageManager/" << GetClientVersion() << 572 " DesktopAppInstaller/" << GetPackageVersion(); 573 return Utility::LocIndString{ strstr.str() }; 574 } 575 576 #ifndef AICLI_DISABLE_TEST_HOOKS 577 void TestHook_SetPathOverride(PathName target, const std::filesystem::path& path) 578 { 579 if (s_Path_TestHook_Overrides.count(target)) 580 { 581 s_Path_TestHook_Overrides[target].Path = path; 582 } 583 else 584 { 585 PathDetails details = GetPathDetailsFor(target); 586 details.Path = path; 587 s_Path_TestHook_Overrides[target] = std::move(details); 588 } 589 } 590 591 void TestHook_SetPathOverride(PathName target, const PathDetails& details) 592 { 593 s_Path_TestHook_Overrides[target] = details; 594 } 595 596 void TestHook_ClearPathOverrides() 597 { 598 s_Path_TestHook_Overrides.clear(); 599 } 600 #endif 601 }