chromium/chrome/installer/util/additional_parameters.cc

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/installer/util/additional_parameters.h"

#include <windows.h>

#include <string_view>

#include "base/check.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/win/registry.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "chrome/install_static/install_details.h"
#include "components/version_info/channel.h"

namespace installer {

namespace {

constexpr wchar_t kRegValueAp[] = L"ap";
constexpr std::wstring_view kFullSuffix = L"-full";
constexpr std::wstring_view kExtendedChannel = L"extended";
const wchar_t kTokenSeparator = L'-';

// Returns null if the value was not found or otherwise could not be read.
std::optional<std::wstring> ReadAdditionalParameters() {
  std::optional<std::wstring> result;
  base::win::RegKey key;

  if (key.Open(install_static::IsSystemInstall() ? HKEY_LOCAL_MACHINE
                                                 : HKEY_CURRENT_USER,
               install_static::GetClientStateKeyPath().c_str(),
               KEY_WOW64_32KEY | KEY_QUERY_VALUE) == ERROR_SUCCESS) {
    result.emplace();
    if (key.ReadValue(kRegValueAp, &result.value()) != ERROR_SUCCESS)
      result.reset();
  }
  return result;
}

// Writes `value` to the "ap" value in the registry, or deletes the "ap" value
// if `value` is null. Returns false and sets the Windows last-error code on
// failure; otherwise, returns true.
bool WriteAdditionalParameters(const std::optional<std::wstring>& value) {
  base::win::RegKey key;
  LONG result = ERROR_SUCCESS;

  if (!value) {
    // Delete the value if it exists.
    result = key.Open(install_static::IsSystemInstall() ? HKEY_LOCAL_MACHINE
                                                        : HKEY_CURRENT_USER,
                      install_static::GetClientStateKeyPath().c_str(),
                      KEY_WOW64_32KEY | KEY_SET_VALUE);
    if (result == ERROR_SUCCESS)
      result = key.DeleteValue(kRegValueAp);
    // Report success if the value was deleted or if either it or the key didn't
    // exist to start with.
    if (result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND ||
        result == ERROR_PATH_NOT_FOUND) {
      return true;
    }
    ::SetLastError(result);
    return false;
  }

  // Write the value to the key.
  result = key.Create(install_static::IsSystemInstall() ? HKEY_LOCAL_MACHINE
                                                        : HKEY_CURRENT_USER,
                      install_static::GetClientStateKeyPath().c_str(),
                      KEY_WOW64_32KEY | KEY_SET_VALUE);
  if (result == ERROR_SUCCESS)
    result = key.WriteValue(kRegValueAp, value->c_str());
  if (result == ERROR_SUCCESS)
    return true;
  ::SetLastError(result);
  return false;
}

bool HasFullSuffix(const std::optional<std::wstring>& value) {
  return value ? base::EndsWith(*value, kFullSuffix) : false;
}

// Expands `channel` to include an optional -arch_FOO suffix, returning true
// if one is found. Returns false without modifying `channel` if none is found.
// `channel` must be a sub-std::string_view of `ap`.
bool SwallowArchSufix(std::wstring_view ap, std::wstring_view& channel) {
  DCHECK_LE(channel.size(), ap.size());
  DCHECK_GE(channel.data(), ap.data());
  DCHECK_LE(channel.data() + channel.size(), ap.data() + ap.size());
  static constexpr std::wstring_view kArchPrefix = L"-arch_";
  auto channel_position = channel.empty() ? 0 : channel.data() - ap.data();
  auto rest_position = channel_position + channel.size();
  if (!base::StartsWith(ap.substr(rest_position), kArchPrefix))
    return false;
  // Retain everything up to the next token separator.
  channel =
      ap.substr(channel_position,
                ap.find(kTokenSeparator, rest_position + kArchPrefix.size()));
  return true;
}

struct ChannelParseState {
  // The canonical name of the parsed channel.
  std::wstring channel_name;

  // The range within the value that matched a channel name pattern.
  std::wstring_view channel_match_range;

  // The range includes an architecture specification (e.g., "x64-beta").
  bool includes_arch;
};

// Parses a channel identifier in `value`, returning the canonical name of the
// channel, the range within `value` that constitutes the identifier, and a
// bool indicating whether or not the identifier also includes an architecture
// specification.
ChannelParseState MakeChannelParseState(
    const std::optional<std::wstring>& value) {
  if (!value)  // No value means stable channel.
    return {std::wstring(), std::wstring_view(), /*includes_arch=*/false};

  std::wstring_view ap(value.value());

  // Expect that "ap" may contain tokens such as "-statsdef_N" and "-full".
  // Historically, all such tokens begin with '-'. Rely on that when matching
  // patterns.

  // Extended stable with an optional -arch_FOO.
  if (base::StartsWith(ap, kExtendedChannel)) {
    std::wstring_view channel_range = ap.substr(0, kExtendedChannel.size());
    bool includes_arch = SwallowArchSufix(ap, channel_range);
    return {std::wstring(kExtendedChannel), channel_range, includes_arch};
  }

  // Beta channel: /^1.1-.*$/ with an optional -arch_FOO. Note that '.' is the
  // regexp wildcard, not a literal period.
  if (ap.size() >= 4 && ap[0] == L'1' && ap[2] == L'1' && ap[3] == L'-') {
    std::wstring_view channel_range = ap.substr(0, ap.find(kTokenSeparator, 4));
    bool includes_arch = SwallowArchSufix(ap, channel_range);
    return {L"beta", channel_range, includes_arch};
  }

  // Dev channel: /^2.0-d.*$/ with an optional -arch_FOO. Note that '.' is the
  // regexp wildcard, not a literal period.
  if (ap.size() >= 5 && ap[0] == L'2' && ap[2] == L'0' && ap[3] == L'-' &&
      ap[4] == L'd') {
    std::wstring_view channel_range = ap.substr(0, ap.find(kTokenSeparator, 5));
    bool includes_arch = SwallowArchSufix(ap, channel_range);
    return {L"dev", channel_range, includes_arch};
  }

  // Older channels.
  static constexpr struct {
    std::wstring_view literal;
    bool is_stable;  // if false, the channel name is embedded in `literal`.
  } kLiteralChannels[] = {
      {L"x64-stable", true}, {L"x86-stable", true}, {L"arm64-stable", true},
      {L"x64-beta", false},  {L"x86-beta", false},  {L"arm64-beta", false},
      {L"x64-dev", false},   {L"x86-dev", false},   {L"arm64-dev", false},
  };
  for (const auto& literal_channel : kLiteralChannels) {
    auto pos = ap.find(literal_channel.literal);
    if (pos == std::wstring_view::npos) {
      continue;
    }
    auto range = ap.substr(pos, literal_channel.literal.size());
    return {literal_channel.is_stable
                ? std::wstring()
                : std::wstring(range.substr(range.find(kTokenSeparator) + 1)),
            range, /*includes_arch=*/true};
  }

  // Explicit stable with an optional -arch_FOO.
  static constexpr std::wstring_view kStableChannel = L"stable";
  if (base::StartsWith(ap, kStableChannel)) {
    std::wstring_view channel_range = ap.substr(0, kStableChannel.size());
    bool includes_arch = SwallowArchSufix(ap, channel_range);
    return {std::wstring(), channel_range, includes_arch};
  }

  // Implicit stable (no channel identifier) with a bare -arch_FOO.
  std::wstring_view channel_range = ap.substr(0, 0);
  if (SwallowArchSufix(ap, channel_range))
    return {std::wstring(), channel_range, /*includes_arch=*/true};

  // Stable channel if nothing else.
  return {std::wstring(), std::wstring_view(), /*includes_arch=*/false};
}

// Returns the channel identifier based on `channel` and
// `is_extended_stable_channel`. If `include_arch`, a processor architecture
// specification is included in the identifier.
std::wstring GetChannelIdentifier(version_info::Channel channel,
                                  bool is_extended_stable_channel,
                                  bool include_arch) {
  static constexpr std::wstring_view kDevChannel = L"2.0-dev";
  static constexpr std::wstring_view kBetaChannel = L"1.1-beta";
#if defined(ARCH_CPU_X86_64)
  static constexpr std::wstring_view kArchSuffix = L"-arch_x64";
#elif defined(ARCH_CPU_X86)
  static constexpr std::wstring_view kArchSuffix = L"-arch_x86";
#elif defined(ARCH_CPU_ARM64)
  static constexpr std::wstring_view kArchSuffix = L"-arch_arm64";
#else
#error unsupported processor architecture.
#endif

  switch (channel) {
    case version_info::Channel::UNKNOWN:
    case version_info::Channel::CANARY:
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
      NOTREACHED_IN_MIGRATION();
#endif
      return std::wstring();

    case version_info::Channel::DEV:
      if (!include_arch)
        return std::wstring(kDevChannel);
      return base::StrCat({kDevChannel, kArchSuffix});

    case version_info::Channel::BETA:
      if (!include_arch)
        return std::wstring(kBetaChannel);
      return base::StrCat({kBetaChannel, kArchSuffix});

    case version_info::Channel::STABLE:
      if (is_extended_stable_channel) {
        if (!include_arch)
          return std::wstring(kExtendedChannel);
        return base::StrCat({kExtendedChannel, kArchSuffix});
      }
      if (!include_arch)
        return std::wstring();
#if defined(ARCH_CPU_X86_64)
      // For historical reasons, this is the ordinary value for 64-bit stable.
      return L"x64-stable";
#elif defined(ARCH_CPU_X86)
      return L"stable-arch_x86";
#elif defined(ARCH_CPU_ARM64)
      return L"arm64-stable";
#else
#error unsupported processor architecture.
#endif
  }
}

}  // namespace

AdditionalParameters::AdditionalParameters()
    : value_(ReadAdditionalParameters()) {}

AdditionalParameters::~AdditionalParameters() = default;

const wchar_t* AdditionalParameters::value() const {
  return value_ ? value_->c_str() : L"";
}

wchar_t AdditionalParameters::GetStatsDefault() const {
  if (!value_)
    return 0;

  static constexpr std::wstring_view kStatsdef = L"-statsdef_";
  std::wstring_view value(*value_);
  auto pos = value.find(kStatsdef);
  if (pos == std::wstring_view::npos) {
    return 0;
  }
  pos += kStatsdef.size();
  return pos < value.size() ? value[pos] : 0;
}

bool AdditionalParameters::SetFullSuffix(bool set_full_suffix) {
  if (HasFullSuffix(value_) == set_full_suffix)
    return false;  // Nothing to do.
  if (set_full_suffix) {
    if (!value_) {
      value_ = std::wstring(kFullSuffix);
    } else {
      value_->append(kFullSuffix);
    }
  } else {
    DCHECK(value_);
    const auto value_size = value_->size();
    if (value_size == kFullSuffix.size()) {
      value_.reset();
    } else {
      value_->resize(value_size - kFullSuffix.size());
    }
  }
  return true;
}

std::wstring AdditionalParameters::ParseChannel() {
  auto parse_state = MakeChannelParseState(value_);
  return std::move(parse_state.channel_name);
}

void AdditionalParameters::SetChannel(version_info::Channel channel,
                                      bool is_extended_stable_channel) {
  auto parse_state = MakeChannelParseState(value_);

  std::wstring new_value = value_ ? *value_ : std::wstring();

  // Remove the old channel and optional architecture identifier.
  if (!parse_state.channel_match_range.empty()) {
    new_value.erase(parse_state.channel_match_range.data() - value_->data(),
                    parse_state.channel_match_range.size());
  }

  // Insert the new channel and architecture identifier (if needed).
  value_ =
      base::StrCat({GetChannelIdentifier(channel, is_extended_stable_channel,
                                         parse_state.includes_arch),
                    new_value});
}

bool AdditionalParameters::Commit() {
  return WriteAdditionalParameters(value_);
}

}  // namespace installer