| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/borealis/borealis_util.h" |
| |
| #include <unordered_set> |
| |
| #include "base/base64.h" |
| #include "base/no_destructor.h" |
| #include "base/process/launch.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/values.h" |
| #include "chrome/browser/ash/borealis/borealis_window_manager.h" |
| #include "chrome/browser/ash/guest_os/guest_os_registry_service.h" |
| #include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/exo/shell_surface_util.h" |
| #include "third_party/re2/src/re2/re2.h" |
| |
| namespace borealis { |
| |
| const char kInstallerAppId[] = "dkecggknbdokeipkgnhifhiokailichf"; |
| const char kClientAppId[] = "epfhbkiklgmlkhfpbcdleadnhcfdjfmo"; |
| const char kLauncherSearchAppId[] = "ceoplblcdaffnnflkkcagjpomjgedmdl"; |
| const char kIgnoredAppIdPrefix[] = "org.chromium.guest_os.borealis.xid."; |
| const char kBorealisDlcName[] = "borealis-dlc"; |
| const char kAllowedScheme[] = "steam"; |
| const re2::LazyRE2 kURLAllowlistRegex[] = { |
| {"//store/[0-9]{1,32}"}, |
| {"//run/[0-9]{1,32}"}, |
| {"//subscriptioninstall/[0-9]{1,32}"}, |
| {"//launch/[0-9]{1,32}/Dialog"}}; |
| const char kCompatToolVersionGameMismatch[] = "UNKNOWN (GameID mismatch)"; |
| const char kDeviceInformationKey[] = "entry.1613887985"; |
| |
| namespace { |
| // Windows with these app IDs are not games. Don't prompt for feedback for them. |
| // |
| // Some Steam updater windows use Zenity to show dialog boxes, and use its |
| // default WMClass. |
| static constexpr char kZenityId[] = |
| "borealis_anon:org.chromium.guest_os.borealis.wmclass.Zenity"; |
| // The Steam client is not a game. |
| static constexpr char kSteamClientId[] = |
| "borealis_anon:org.chromium.guest_os.borealis.wmclass.steam"; |
| // 769 is the Steam App ID assigned to the Steam Big Picture client as of 2023. |
| static constexpr char kSteamBigPictureId[] = |
| "borealis_anon:org.chromium.guest_os.borealis.xprop.769"; |
| |
| // The regex used for extracting the "steam game id" of a .desktop's "Exec=" |
| // field. |
| const re2::LazyRE2 kSteamGameIdFromExecRegex = { |
| "steam:\\/\\/rungameid\\/(\\d+)"}; |
| // The regex used for extracting the "steam game id" of a borealis window (or |
| // anonymous app). |
| const re2::LazyRE2 kSteamGameIdFromWindowRegex = { |
| "org\\.chromium\\.guest_os\\.borealis\\.xprop\\.(\\d+)"}; |
| |
| // Works for window-data either in the exo_id form, or the anonymous app_id |
| // form. |
| std::optional<int> ParseGameIdFromWindowData(const std::string& data) { |
| int app_id; |
| if (RE2::PartialMatch(data, *kSteamGameIdFromWindowRegex, &app_id)) { |
| return app_id; |
| } |
| return std::nullopt; |
| } |
| |
| // Regexes attempting to match known and potential future Proton/SLR versions, |
| // used to prevent these tools showing up in the launcher. |
| // These are intended to be as future-proof as practical without matching too |
| // broadly. In particular, there are actual games named "Proton <Word>". |
| const re2::LazyRE2 kSpuriousGameBlocklist[] = { |
| {"Proton [0-9.]+"}, |
| {"Proton BattlEye Runtime"}, |
| {"Proton EasyAntiCheat Runtime"}, |
| {"Proton Experimental"}, |
| {"Proton Hotfix"}, |
| {"Proton Next"}, |
| {"Steam Linux Runtime.*"}, |
| }; |
| |
| // Additionally block non-games by Steam App ID, in case the tool names are |
| // changed. This is not future-proof, but provides an additional layer of |
| // defence for known tools. |
| bool IsSteamTool(int id) { |
| static const base::NoDestructor<std::unordered_set<int>> kSteamToolIds({ |
| 1070560, // Steam Linux Runtime 1.0 (scout) |
| 1391110, // Steam Linux Runtime 2.0 (soldier) |
| 1628350, // Steam Linux Runtime 3.0 (sniper) |
| 858280, // Proton 3.7 |
| 930400, // Proton 3.7 Beta |
| 961940, // Proton 3.16 |
| 996510, // Proton 3.16 Beta |
| 1054830, // Proton 4.2 |
| 1113280, // Proton 4.11 |
| 1161040, // Proton BattlEye Runtime |
| 1245040, // Proton 5.0 |
| 1420170, // Proton 5.13 |
| 1493710, // Proton Experimental |
| 1580130, // Proton 6.3 |
| 1826330, // Proton EasyAntiCheat Runtime |
| 1887720, // Proton 7.0 |
| 2180100, // Proton Hotfix |
| 2230260, // Proton Next |
| 2348590, // Proton 8.0 |
| }); |
| return kSteamToolIds->contains(id); |
| } |
| |
| } // namespace |
| |
| std::optional<int> ParseSteamGameId(std::string exec) { |
| int app_id; |
| if (RE2::PartialMatch(exec, *kSteamGameIdFromExecRegex, &app_id)) { |
| return app_id; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<int> SteamGameId(const aura::Window* window) { |
| const std::string* id = exo::GetShellApplicationId(window); |
| if (!id) { |
| return std::nullopt; |
| } |
| return ParseGameIdFromWindowData(*id); |
| } |
| |
| std::optional<int> SteamGameId(Profile* profile, const std::string& app_id) { |
| if (BorealisWindowManager::IsAnonymousAppId(app_id)) { |
| return ParseGameIdFromWindowData(app_id); |
| } |
| guest_os::GuestOsRegistryService* registry = |
| guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile); |
| if (!registry) { |
| return std::nullopt; |
| } |
| std::optional<guest_os::GuestOsRegistryService::Registration> reg = |
| registry->GetRegistration(app_id); |
| if (!reg) { |
| return std::nullopt; |
| } |
| return ParseSteamGameId(reg->Exec()); |
| } |
| |
| bool IsNonGameBorealisApp(const std::string& app_id) { |
| if (app_id.find(kIgnoredAppIdPrefix) != std::string::npos || |
| app_id == kClientAppId) { |
| return true; |
| } |
| |
| if (app_id == kZenityId || app_id == kSteamClientId || |
| app_id == kSteamBigPictureId) { |
| return true; |
| } |
| return false; |
| } |
| |
| bool ShouldHideIrrelevantApp( |
| const guest_os::GuestOsRegistryService::Registration& registration) { |
| std::optional<int> id = ParseSteamGameId(registration.Exec()); |
| if (id && IsSteamTool(id.value())) { |
| return true; |
| } |
| for (auto& blocklist_regex : kSpuriousGameBlocklist) { |
| if (re2::RE2::FullMatch(registration.Name(), *blocklist_regex)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool IsExternalURLAllowed(const GURL& url) { |
| if (url.scheme() != kAllowedScheme) { |
| return false; |
| } |
| for (auto& allowed_url : kURLAllowlistRegex) { |
| if (re2::RE2::FullMatch(url.GetContent(), *allowed_url)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| bool GetCompatToolInfo(const std::string& owner_id, std::string* output) { |
| std::vector<std::string> command = {"/usr/bin/vsh", "--owner_id=" + owner_id, |
| "--vm_name=borealis", "--", |
| "/usr/bin/get_compat_tool_versions.py"}; |
| return base::GetAppOutputAndError(command, output); |
| } |
| |
| CompatToolInfo ParseCompatToolInfo(std::optional<int> game_id, |
| const std::string& output) { |
| // Expected stdout of get_compat_tool_versions.py: |
| // GameID: <game_id>, Proton:<proton_version>, SLR: <slr_version>, Timestamp: <timestamp> |
| // GameID: <game_id>, Proton:<proton_version>, SLR: <slr_version>, Timestamp: <timestamp> |
| // ... |
| |
| // Only grab the first line, which is for the last game played. |
| std::string raw_info = output.substr(0, output.find("\n")); |
| |
| CompatToolInfo compat_tool_info; |
| base::StringPairs tokenized_info; |
| base::SplitStringIntoKeyValuePairs(raw_info, ':', ',', &tokenized_info); |
| for (const auto& key_val_pair : tokenized_info) { |
| std::string key; |
| TrimWhitespaceASCII(key_val_pair.first, base::TRIM_ALL, &key); |
| |
| std::string val; |
| TrimWhitespaceASCII(key_val_pair.second, base::TRIM_ALL, &val); |
| |
| if (key == "GameID") { |
| int parsed_val; |
| bool ret = base::StringToInt(val, &parsed_val); |
| if (ret) { |
| compat_tool_info.game_id = parsed_val; |
| } |
| } else if (key == "Proton") { |
| compat_tool_info.proton = val; |
| } else if (key == "SLR") { |
| compat_tool_info.slr = val; |
| } |
| } |
| |
| // If the app id is known and doesn't match, return the version "UNKNOWN" |
| if (game_id.has_value() && compat_tool_info.game_id.has_value() && |
| game_id.value() != compat_tool_info.game_id.value()) { |
| LOG(WARNING) << "Expected GameID " << game_id.value() << " got " |
| << compat_tool_info.game_id.value(); |
| compat_tool_info.proton = kCompatToolVersionGameMismatch; |
| compat_tool_info.slr = kCompatToolVersionGameMismatch; |
| } |
| |
| return compat_tool_info; |
| } |
| |
| } // namespace borealis |