chromium/chrome/browser/ash/borealis/borealis_util.cc

// 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_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