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

#include "base/win/win_util.h"

#include <objbase.h>

#include <winternl.h>

#include <ntstatus.h>

#include <algorithm>
#include <string_view>
#include <utility>

#include "base/files/file_path.h"
#include "base/process/process.h"
#include "base/process/process_handle.h"
#include "base/scoped_environment_variable_override.h"
#include "base/scoped_native_library.h"
#include "base/strings/cstring_view.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_expected_support.h"
#include "base/threading/platform_thread.h"
#include "base/win/registry.h"
#include "base/win/scoped_co_mem.h"
#include "base/win/scoped_com_initializer.h"
#include "base/win/windows_handle_util.h"
#include "base/win/windows_version.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace base {
namespace win {

namespace {

// Saves the current thread's locale ID when initialized, and restores it when
// the instance is going out of scope.
class ThreadLocaleSaver {
 public:
  ThreadLocaleSaver() : original_locale_id_(GetThreadLocale()) {}

  ThreadLocaleSaver(const ThreadLocaleSaver&) = delete;
  ThreadLocaleSaver& operator=(const ThreadLocaleSaver&) = delete;

  ~ThreadLocaleSaver() { SetThreadLocale(original_locale_id_); }

 private:
  LCID original_locale_id_;
};

auto* csm_false = static_cast<bool (*)()>([]() -> bool { return false; });

auto* csm_true = static_cast<bool (*)()>([]() -> bool { return true; });

void TestUnicodeStringToView(wcstring_view test) {
  UNICODE_STRING teststr = {};
  ::RtlInitUnicodeString(&teststr, test.c_str());
  std::wstring_view view = UnicodeStringToView(teststr);
  EXPECT_EQ(view, test);
  // Pointer comparison.
  EXPECT_EQ(view.data(), test.data());
  EXPECT_EQ(std::size(view), std::size(test));
}

void TestViewToUnicodeString(std::wstring_view view) {
  UNICODE_STRING str;
  EXPECT_TRUE(ViewToUnicodeString(view, str));
  // Pointer comparison.
  EXPECT_EQ(str.Buffer, view.data());
  EXPECT_EQ(str.Length, view.size() * sizeof(WCHAR));
  EXPECT_EQ(str.Length, str.MaximumLength);
}

}  // namespace

// The test is somewhat silly, because some bots some have UAC enabled and some
// have it disabled. At least we check that it does not crash.
TEST(BaseWinUtilTest, TestIsUACEnabled) {
  UserAccountControlIsEnabled();
}

TEST(BaseWinUtilTest, TestGetUserSidString) {
  std::wstring user_sid;
  EXPECT_TRUE(GetUserSidString(&user_sid));
  EXPECT_TRUE(!user_sid.empty());
}

TEST(BaseWinUtilTest, TestGetLoadedModulesSnapshot) {
  std::vector<HMODULE> snapshot;

  ASSERT_TRUE(GetLoadedModulesSnapshot(::GetCurrentProcess(), &snapshot));
  size_t original_snapshot_size = snapshot.size();
  ASSERT_GT(original_snapshot_size, 0u);
  snapshot.clear();

  // Load in a new module. Pick zipfldr.dll as it is present from WinXP to
  // Win10, including ARM64 Win10, and yet rarely used.
  const FilePath::CharType dll_name[] = FILE_PATH_LITERAL("zipfldr.dll");
  ASSERT_EQ(nullptr, ::GetModuleHandle(dll_name));

  ScopedNativeLibrary new_dll((FilePath(dll_name)));
  ASSERT_NE(static_cast<HMODULE>(nullptr), new_dll.get());
  ASSERT_TRUE(GetLoadedModulesSnapshot(::GetCurrentProcess(), &snapshot));
  ASSERT_GT(snapshot.size(), original_snapshot_size);
  ASSERT_TRUE(std::ranges::contains(snapshot, new_dll.get()));
}

TEST(BaseWinUtilTest, TestUint32ToInvalidHandle) {
  // Ensure that INVALID_HANDLE_VALUE is preserved when going to a 32-bit value
  // and back on 64-bit platforms.
  uint32_t invalid_handle = HandleToUint32(INVALID_HANDLE_VALUE);
  EXPECT_EQ(INVALID_HANDLE_VALUE, Uint32ToHandle(invalid_handle));
}

TEST(BaseWinUtilTest, PseudoHandles) {
  EXPECT_TRUE(IsPseudoHandle(::GetCurrentProcess()));
  EXPECT_TRUE(IsPseudoHandle(::GetCurrentThread()));
  EXPECT_FALSE(IsPseudoHandle(nullptr));
}

TEST(BaseWinUtilTest, WStringFromGUID) {
  const GUID kGuid = {0x7698f759,
                      0xf5b0,
                      0x4328,
                      {0x92, 0x38, 0xbd, 0x70, 0x8a, 0x6d, 0xc9, 0x63}};
  const std::wstring_view kGuidStr = L"{7698F759-F5B0-4328-9238-BD708A6DC963}";
  auto guid_wstring = WStringFromGUID(kGuid);
  EXPECT_EQ(guid_wstring, kGuidStr);
  wchar_t guid_wchar[39];
  ::StringFromGUID2(kGuid, guid_wchar, std::size(guid_wchar));
  EXPECT_STREQ(guid_wstring.c_str(), guid_wchar);
  ScopedCoMem<OLECHAR> clsid_string;
  ::StringFromCLSID(kGuid, &clsid_string);
  EXPECT_STREQ(guid_wstring.c_str(), clsid_string.get());
}

TEST(BaseWinUtilTest, GetWindowObjectName) {
  std::wstring created_desktop_name(L"test_desktop");
  HDESK desktop_handle =
      ::CreateDesktop(created_desktop_name.c_str(), nullptr, nullptr, 0,
                      DESKTOP_CREATEWINDOW | DESKTOP_READOBJECTS |
                          READ_CONTROL | WRITE_DAC | WRITE_OWNER,
                      nullptr);

  ASSERT_NE(desktop_handle, nullptr);
  EXPECT_EQ(created_desktop_name, GetWindowObjectName(desktop_handle));
  ASSERT_TRUE(::CloseDesktop(desktop_handle));
}

TEST(BaseWinUtilTest, IsRunningUnderDesktopName) {
  HDESK thread_desktop = ::GetThreadDesktop(::GetCurrentThreadId());

  ASSERT_NE(thread_desktop, nullptr);
  std::wstring desktop_name = GetWindowObjectName(thread_desktop);

  EXPECT_TRUE(IsRunningUnderDesktopName(desktop_name));
  EXPECT_TRUE(IsRunningUnderDesktopName(ToLowerASCII(desktop_name)));
  EXPECT_TRUE(IsRunningUnderDesktopName(ToUpperASCII(desktop_name)));
  EXPECT_FALSE(
      IsRunningUnderDesktopName(desktop_name + L"_non_existent_desktop_name"));
}

TEST(BaseWinUtilTest, ExpandEnvironmentVariables) {
  constexpr char kTestEnvVar[] = "TEST_ENV_VAR";
  constexpr char kTestEnvVarValue[] = "TEST_VALUE";
  ScopedEnvironmentVariableOverride scoped_env(kTestEnvVar, kTestEnvVarValue);

  auto path_with_env_var = UTF8ToWide(std::string("C:\\%") + kTestEnvVar + "%");
  auto path_expanded = UTF8ToWide(std::string("C:\\") + kTestEnvVarValue);

  EXPECT_EQ(ExpandEnvironmentVariables(path_with_env_var).value(),
            path_expanded);
}

TEST(BaseWinUtilTest, ExpandEnvironmentVariablesEmptyValue) {
  constexpr char kTestEnvVar[] = "TEST_ENV_VAR";
  constexpr char kTestEnvVarValue[] = "";
  ScopedEnvironmentVariableOverride scoped_env(kTestEnvVar, kTestEnvVarValue);

  auto path_with_env_var = UTF8ToWide(std::string("C:\\%") + kTestEnvVar + "%");
  auto path_expanded = UTF8ToWide(std::string("C:\\") + kTestEnvVarValue);

  EXPECT_EQ(ExpandEnvironmentVariables(path_with_env_var).value(),
            path_expanded);
}

TEST(BaseWinUtilTest, ExpandEnvironmentVariablesUndefinedValue) {
  constexpr char kTestEnvVar[] = "TEST_ENV_VAR";

  auto path_with_env_var = UTF8ToWide(std::string("C:\\%") + kTestEnvVar + "%");

  // Undefined env vars are left unexpanded.
  auto path_expanded = path_with_env_var;

  EXPECT_EQ(ExpandEnvironmentVariables(path_with_env_var).value(),
            path_expanded);
}

TEST(BaseWinUtilTest, ProcessPowerThrottling) {
  if (GetVersion() < Version::WIN11_22H2) {
    GTEST_SKIP() << "Test only applies to Windows 11 22H2 and later.";
  }

  // Clear any previous state.
  ASSERT_TRUE(
      SetProcessEcoQoSState(::GetCurrentProcess(), ProcessPowerState::kUnset));
  ASSERT_TRUE(SetProcessTimerThrottleState(::GetCurrentProcess(),
                                           ProcessPowerState::kUnset));

  // Verify the initial state.
  ASSERT_TRUE(GetProcessEcoQoSState(::GetCurrentProcess()) ==
              ProcessPowerState::kUnset);
  ASSERT_TRUE(GetProcessTimerThrottleState(::GetCurrentProcess()) ==
              ProcessPowerState::kUnset);

  // Verify setting the EcoQoS state.
  ASSERT_TRUE(SetProcessEcoQoSState(::GetCurrentProcess(),
                                    ProcessPowerState::kEnabled));
  ASSERT_TRUE(GetProcessEcoQoSState(::GetCurrentProcess()) ==
              ProcessPowerState::kEnabled);
  ASSERT_TRUE(
      SetProcessEcoQoSState(::GetCurrentProcess(), ProcessPowerState::kUnset));

  // Verify setting the timer resolution state.
  ASSERT_TRUE(SetProcessTimerThrottleState(::GetCurrentProcess(),
                                           ProcessPowerState::kEnabled));
  ASSERT_TRUE(GetProcessTimerThrottleState(::GetCurrentProcess()) ==
              ProcessPowerState::kEnabled);

  // Set the EcoQoS state again and verify the timer throttling state is not
  // clobbered.
  ASSERT_TRUE(SetProcessEcoQoSState(::GetCurrentProcess(),
                                    ProcessPowerState::kEnabled));
  ASSERT_TRUE(GetProcessEcoQoSState(::GetCurrentProcess()) ==
              ProcessPowerState::kEnabled);
  ASSERT_TRUE(GetProcessTimerThrottleState(::GetCurrentProcess()) ==
              ProcessPowerState::kEnabled);

  // Disable the EcoQoS state and verify the timer throttling state is not
  // clobbered.
  ASSERT_TRUE(SetProcessEcoQoSState(::GetCurrentProcess(),
                                    ProcessPowerState::kDisabled));
  ASSERT_TRUE(GetProcessEcoQoSState(::GetCurrentProcess()) ==
              ProcessPowerState::kDisabled);
  ASSERT_TRUE(GetProcessTimerThrottleState(::GetCurrentProcess()) ==
              ProcessPowerState::kEnabled);

  // Disable the timer throttling state and verify state.
  ASSERT_TRUE(SetProcessTimerThrottleState(::GetCurrentProcess(),
                                           ProcessPowerState::kDisabled));
  ASSERT_TRUE(GetProcessTimerThrottleState(::GetCurrentProcess()) ==
              ProcessPowerState::kDisabled);
  ASSERT_TRUE(GetProcessEcoQoSState(::GetCurrentProcess()) ==
              ProcessPowerState::kDisabled);

  // Enable both states and verify.
  ASSERT_TRUE(SetProcessEcoQoSState(::GetCurrentProcess(),
                                    ProcessPowerState::kEnabled));
  ASSERT_TRUE(SetProcessTimerThrottleState(::GetCurrentProcess(),
                                           ProcessPowerState::kEnabled));
  ASSERT_TRUE(GetProcessEcoQoSState(::GetCurrentProcess()) ==
              ProcessPowerState::kEnabled);
  ASSERT_TRUE(GetProcessTimerThrottleState(::GetCurrentProcess()) ==
              ProcessPowerState::kEnabled);

  // Clear both states and verify.
  ASSERT_TRUE(
      SetProcessEcoQoSState(::GetCurrentProcess(), ProcessPowerState::kUnset));
  ASSERT_TRUE(SetProcessTimerThrottleState(::GetCurrentProcess(),
                                           ProcessPowerState::kUnset));
  ASSERT_TRUE(GetProcessEcoQoSState(::GetCurrentProcess()) ==
              ProcessPowerState::kUnset);
  ASSERT_TRUE(GetProcessTimerThrottleState(::GetCurrentProcess()) ==
              ProcessPowerState::kUnset);
}

TEST(GetObjectTypeNameTest, NullHandle) {
  auto name_or_error = GetObjectTypeName(kNullProcessHandle);
  ASSERT_FALSE(name_or_error.has_value());
  ASSERT_EQ(name_or_error.error(), STATUS_INVALID_HANDLE);
}

TEST(GetObjectTypeNameTest, InvalidHandle) {
  auto name_or_error = GetObjectTypeName(INVALID_HANDLE_VALUE);
  ASSERT_FALSE(name_or_error.has_value());
  ASSERT_EQ(name_or_error.error(), STATUS_INVALID_HANDLE);
}

TEST(GetObjectTypeNameTest, CurrentProcess) {
  auto name_or_error = GetObjectTypeName(::GetCurrentProcess());
  ASSERT_FALSE(name_or_error.has_value());
  ASSERT_EQ(name_or_error.error(), STATUS_INVALID_HANDLE);
}

TEST(GetObjectTypeNameDeathTest, CrazyHandle) {
  EXPECT_DEATH_IF_SUPPORTED(
      std::ignore = GetObjectTypeName(Uint32ToHandle(0x12345678U)),
      "Received fatal exception 0xc0000008");
}

TEST(GetObjectTypeNameTest, ProcessHandle) {
  Process this_process = Process::Open(GetCurrentProcId());
  ASSERT_OK_AND_ASSIGN(std::wstring type_name,
                       GetObjectTypeName(this_process.Handle()));
  ASSERT_EQ(type_name, L"Process");
}

TEST(DeviceConvertibilityTest, None) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  ScopedDeviceConvertibilityStateForTesting scoper(false, false, csm_false,
                                                   std::nullopt, std::nullopt);
  EXPECT_FALSE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, ConvertibilityDisabled) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  // If convertibility is not enabled but the key exists, other values shouldn't
  // be checked. Device is not convertible.
  ScopedDeviceConvertibilityStateForTesting scoper(
      /*form_convertible=*/true, /*chassis_convertible=*/true,
      /*csm_changed=*/csm_false, /*convertible_chassis_key=*/std::nullopt,
      /*convertibility_enabled=*/std::optional<bool>{false});
  EXPECT_FALSE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, ConvertibilityEnabled) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  ScopedDeviceConvertibilityStateForTesting scoper(
      /*form_convertible=*/false, /*chassis_convertible=*/false,
      /*csm_changed=*/csm_false, /*convertible_chassis_key=*/std::nullopt,
      /*convertibility_enabled=*/std::optional<bool>{true});
  EXPECT_TRUE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, ChassisConvertibleKeyTrue) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  ScopedDeviceConvertibilityStateForTesting scoper(
      /*form_convertible=*/false, /*chassis_convertible=*/false,
      /*csm_changed=*/csm_false,
      /*convertible_chassis_key=*/std::optional<bool>{true},
      /*convertibility_enabled=*/std::nullopt);
  EXPECT_TRUE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, ChassisConvertibleKeyFalse) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  ScopedDeviceConvertibilityStateForTesting scoper(
      /*form_convertible=*/false, /*chassis_convertible=*/true,
      /*csm_changed=*/csm_true,
      /*convertible_chassis_key=*/std::optional<bool>{false},
      /*convertibility_enabled=*/std::nullopt);
  EXPECT_FALSE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, FormConvertibleTrue) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  ScopedDeviceConvertibilityStateForTesting scoper(
      /*form_convertible=*/true, /*chassis_convertible=*/false,
      /*csm_changed=*/csm_false,
      /*convertible_chassis_key=*/std::nullopt,
      /*convertibility_enabled=*/std::nullopt);
  EXPECT_TRUE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, ChassisConvertibleTrue) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  ScopedDeviceConvertibilityStateForTesting scoper(
      /*form_convertible=*/false, /*chassis_convertible=*/true,
      /*csm_changed=*/csm_false,
      /*convertible_chassis_key=*/std::nullopt,
      /*convertibility_enabled=*/std::nullopt);
  EXPECT_TRUE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, ConvertibleSlateModeChangeTrue) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  ScopedDeviceConvertibilityStateForTesting scoper(
      /*form_convertible=*/false, /*chassis_convertible=*/false,
      /*csm_changed=*/csm_true,
      /*convertible_chassis_key=*/std::nullopt,
      /*convertibility_enabled=*/std::nullopt);
  EXPECT_TRUE(QueryDeviceConvertibility());
}

TEST(DeviceConvertibilityTest, ConvertibilityEnabledSanityCheck) {
  RegKey key(HKEY_LOCAL_MACHINE,
             L"System\\CurrentControlSet\\Control\\PriorityControl", KEY_READ);
  if (key.HasValue(L"ConvertibilityEnabled")) {
    ASSERT_TRUE(GetConvertibilityEnabledOverride().has_value());
  } else {
    ASSERT_FALSE(GetConvertibilityEnabledOverride().has_value());
  }
}

TEST(DeviceConvertibilityTest, ConvertibilityKeySanityCheck) {
  RegKey key(HKEY_CURRENT_USER,
             L"SOFTWARE\\Microsoft\\TabletTip\\ConvertibleChassis", KEY_READ);
  if (key.HasValue(L"ConvertibleChassis")) {
    ASSERT_TRUE(GetConvertibleChassisKeyValue().has_value());
  } else {
    ASSERT_FALSE(GetConvertibleChassisKeyValue().has_value());
  }
}

TEST(DeviceConvertibilityTest, DeviceFormAndChassisConvertible) {
  ScopedCOMInitializer com_initializer;
  ASSERT_TRUE(com_initializer.Succeeded());
  EXPECT_FALSE(IsDeviceFormConvertible() || IsChassisConvertible());
}

TEST(BaseWinUtilTest, GetSerialNumber) {
  ScopedCOMInitializer com_initializer;
  ASSERT_OK_AND_ASSIGN(std::wstring serial_number, GetSerialNumber());
  EXPECT_FALSE(serial_number.empty());
}

TEST(BaseWinUtilTest, UnicodeStringToView) {
  UNICODE_STRING nullstr = {};
  EXPECT_TRUE(UnicodeStringToView(nullstr).empty());
  TestUnicodeStringToView(L"");
  TestUnicodeStringToView(L"ThisIsATestString");
  TestUnicodeStringToView(std::wstring((UINT16_MAX / sizeof(WCHAR)) - 1, L'A'));
}

TEST(BaseWinUtilTest, ViewToUnicodeString) {
  TestViewToUnicodeString({});
  TestViewToUnicodeString(L"");
  TestViewToUnicodeString(L"ThisIsATestString");
  std::wstring long_str(UINT16_MAX / sizeof(WCHAR), L'A');
  TestViewToUnicodeString(long_str);
  long_str += L"A";
  UNICODE_STRING invalid = {};
  EXPECT_FALSE(ViewToUnicodeString(long_str, invalid));
}

// This policy is set in `TestSuite::Initialize` for all tests so this test
// checks that it takes effect here.
TEST(BaseWinUtilTest, StrictHandleChecks) {
  PROCESS_MITIGATION_STRICT_HANDLE_CHECK_POLICY policy = {};
  ASSERT_TRUE(::GetProcessMitigationPolicy(::GetCurrentProcess(),
                                           ProcessStrictHandleCheckPolicy,
                                           &policy, sizeof(policy)));
  EXPECT_TRUE(policy.HandleExceptionsPermanentlyEnabled);
  EXPECT_TRUE(policy.RaiseExceptionOnInvalidHandleReference);
}

}  // namespace win
}  // namespace base
