mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-14 22:56:12 -04:00
Move registry accessor code to nearby platform.
PiperOrigin-RevId: 852969284
This commit is contained in:
committed by
Copybara-Service
parent
74c921f911
commit
d302e8459d
@@ -199,6 +199,48 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "registry_accessor",
|
||||
hdrs = ["registry_accessor.h"],
|
||||
tags = ["windows"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "registry",
|
||||
srcs = [
|
||||
"registry.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"registry.h",
|
||||
],
|
||||
linkopts = [
|
||||
"shlwapi.lib",
|
||||
],
|
||||
tags = ["windows"],
|
||||
visibility = [
|
||||
"//internal/platform/implementation/windows:__subpackages__",
|
||||
"//location/nearby:__subpackages__",
|
||||
"//sharing/internal/impl/windows:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":registry_accessor",
|
||||
"//internal/platform:logging",
|
||||
"@com_google_absl//absl/container:flat_hash_map",
|
||||
"@com_google_absl//absl/strings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "mock_registry_accessor",
|
||||
testonly = True,
|
||||
hdrs = ["mock_registry_accessor.h"],
|
||||
tags = ["windows"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":registry_accessor",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "windows",
|
||||
srcs = [
|
||||
@@ -564,3 +606,18 @@ cc_test(
|
||||
"@com_google_googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "registry_test",
|
||||
size = "small",
|
||||
timeout = "short",
|
||||
srcs = [
|
||||
"registry_test.cc",
|
||||
],
|
||||
deps = [
|
||||
":mock_registry_accessor",
|
||||
":registry",
|
||||
"@com_github_protobuf_matchers//protobuf-matchers",
|
||||
"@com_google_googletest//:gtest_main",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_MOCK_REGISTRY_ACCESSOR_H_
|
||||
#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_MOCK_REGISTRY_ACCESSOR_H_
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "internal/platform/implementation/windows/registry_accessor.h"
|
||||
|
||||
namespace nearby::platform::windows {
|
||||
|
||||
class MockRegistryAccessor : public RegistryAccessor {
|
||||
public:
|
||||
MockRegistryAccessor() = default;
|
||||
MockRegistryAccessor(const MockRegistryAccessor&) = delete;
|
||||
MockRegistryAccessor& operator=(const MockRegistryAccessor&) = delete;
|
||||
~MockRegistryAccessor() override = default;
|
||||
|
||||
MOCK_METHOD(LSTATUS, ReadDWordValue,
|
||||
(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name, DWORD& value_data),
|
||||
(override));
|
||||
MOCK_METHOD(LSTATUS, ReadStringValue,
|
||||
(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name, std::string& value_data),
|
||||
(override));
|
||||
MOCK_METHOD(LSTATUS, WriteDWordValue,
|
||||
(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name, DWORD value_data,
|
||||
bool create_sub_key),
|
||||
(override));
|
||||
MOCK_METHOD(LSTATUS, WriteStringValue,
|
||||
(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name, const std::string& value_data,
|
||||
bool create_sub_key),
|
||||
(override));
|
||||
MOCK_METHOD(LSTATUS, EnumValues,
|
||||
(HKEY key, const std::string& sub_key,
|
||||
(absl::flat_hash_map<std::string, std::string>)& string_values,
|
||||
(absl::flat_hash_map<std::string, DWORD>)& dword_values),
|
||||
(override));
|
||||
};
|
||||
|
||||
} // namespace nearby::platform::windows
|
||||
|
||||
#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_MOCK_REGISTRY_ACCESSOR_H_
|
||||
@@ -0,0 +1,447 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "internal/platform/implementation/windows/registry.h"
|
||||
|
||||
#define _WIN32_WINNT _WIN32_WINNT_WIN10
|
||||
|
||||
#include <windows.h> // NOLINT
|
||||
#include <winreg.h> // NOLINT
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "internal/platform/implementation/windows/registry_accessor.h"
|
||||
#include "internal/platform/logging.h"
|
||||
|
||||
namespace nearby::platform::windows {
|
||||
|
||||
namespace {
|
||||
constexpr int kMaxRegistryStringValueBufferSize = 1024;
|
||||
constexpr absl::string_view kGoogleUpdateClientsKey =
|
||||
R"(Google\Update\Clients\)";
|
||||
constexpr absl::string_view kGoogleUpdateClientStateKey =
|
||||
R"(Google\Update\ClientState\)";
|
||||
constexpr absl::string_view kGoogleUpdateClientStateMediumKey =
|
||||
R"(Google\Update\ClientStateMedium\)";
|
||||
constexpr absl::string_view kGoogleNearbyShareKey = R"(Google\NearbyShare)";
|
||||
constexpr absl::string_view kGoogleNearbyShareFlagsKey =
|
||||
R"(Google\NearbyShare\Flags)";
|
||||
constexpr absl::string_view kWindowsKey =
|
||||
R"(Microsoft\Windows NT\CurrentVersion)";
|
||||
|
||||
constexpr absl::string_view kHiveRootSoftware = "SOFTWARE";
|
||||
constexpr absl::string_view kHiveRootSoftwareWow6432Node =
|
||||
"SOFTWARE\\WOW6432Node";
|
||||
constexpr absl::string_view kHiveRootSystem = "SYSTEM";
|
||||
|
||||
constexpr absl::string_view kQuickShareAppProductId =
|
||||
"{232066FE-FF4D-4C25-83B4-3F8747CF7E3A}";
|
||||
|
||||
// RegistryAccessor for actual Win API implementation.
|
||||
class WindowsRegistryAccessor : public RegistryAccessor {
|
||||
public:
|
||||
WindowsRegistryAccessor() = default;
|
||||
~WindowsRegistryAccessor() override = default;
|
||||
WindowsRegistryAccessor(const WindowsRegistryAccessor&) = delete;
|
||||
WindowsRegistryAccessor& operator=(const WindowsRegistryAccessor&) = delete;
|
||||
|
||||
LSTATUS ReadDWordValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name,
|
||||
DWORD& value_data) override {
|
||||
DWORD value_size = sizeof(DWORD);
|
||||
return RegGetValueA(
|
||||
key, sub_key.data(), value_name.data(),
|
||||
RRF_RT_DWORD | RRF_ZEROONFAILURE | RRF_SUBKEY_WOW6432KEY,
|
||||
/*pdwType=*/nullptr, static_cast<void*>(&value_data), &value_size);
|
||||
};
|
||||
|
||||
LSTATUS ReadStringValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name,
|
||||
std::string& value_data) override {
|
||||
char value_buffer[kMaxRegistryStringValueBufferSize];
|
||||
DWORD value_size = kMaxRegistryStringValueBufferSize;
|
||||
LSTATUS status = RegGetValueA(
|
||||
key, sub_key.data(), value_name.data(),
|
||||
RRF_RT_REG_SZ | RRF_ZEROONFAILURE | RRF_SUBKEY_WOW6432KEY,
|
||||
/*pdwType=*/nullptr, static_cast<void*>(&value_buffer), &value_size);
|
||||
if (status == ERROR_SUCCESS) {
|
||||
value_data = value_buffer;
|
||||
}
|
||||
return status;
|
||||
};
|
||||
|
||||
LSTATUS WriteDWordValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name, DWORD value_data,
|
||||
bool create_sub_key) override {
|
||||
HKEY h_sub_key;
|
||||
LSTATUS status = RegOpenKeyExA(key, sub_key.data(), /*ulOptions=*/0,
|
||||
KEY_WRITE | KEY_WOW64_32KEY, &h_sub_key);
|
||||
if (create_sub_key && status == ERROR_FILE_NOT_FOUND) {
|
||||
status = RegCreateKeyExA(key, sub_key.data(), /*reserved*/ 0,
|
||||
/*lpClass=*/nullptr, REG_OPTION_NON_VOLATILE,
|
||||
KEY_WRITE | KEY_WOW64_32KEY,
|
||||
/*lpSecurityAttributes=*/nullptr, &h_sub_key,
|
||||
/*lpdwDisposition=*/nullptr);
|
||||
}
|
||||
if (status != ERROR_SUCCESS) {
|
||||
return status;
|
||||
}
|
||||
return RegSetKeyValueA(h_sub_key, /*lpSubKey=*/nullptr, value_name.data(),
|
||||
REG_DWORD, static_cast<const void*>(&value_data),
|
||||
sizeof(DWORD));
|
||||
}
|
||||
|
||||
LSTATUS WriteStringValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name,
|
||||
const std::string& value_data,
|
||||
bool create_sub_key) override {
|
||||
HKEY h_sub_key;
|
||||
LSTATUS status = RegOpenKeyExA(key, sub_key.data(), /*ulOptions=*/0,
|
||||
KEY_WRITE | KEY_WOW64_32KEY, &h_sub_key);
|
||||
if (create_sub_key && status == ERROR_FILE_NOT_FOUND) {
|
||||
status = RegCreateKeyExA(key, sub_key.data(), /*reserved*/ 0,
|
||||
/*lpClass=*/nullptr, REG_OPTION_NON_VOLATILE,
|
||||
KEY_WRITE | KEY_WOW64_32KEY,
|
||||
/*lpSecurityAttributes=*/nullptr, &h_sub_key,
|
||||
/*lpdwDisposition=*/nullptr);
|
||||
}
|
||||
if (status != ERROR_SUCCESS) {
|
||||
return status;
|
||||
}
|
||||
return RegSetKeyValueA(h_sub_key, /*lpSubKey=*/nullptr, value_name.data(),
|
||||
REG_SZ, static_cast<const void*>(value_data.data()),
|
||||
value_data.size() + 1);
|
||||
}
|
||||
|
||||
LSTATUS EnumValues(
|
||||
HKEY key, const std::string& sub_key,
|
||||
absl::flat_hash_map<std::string, std::string>& string_values,
|
||||
absl::flat_hash_map<std::string, DWORD>& dword_values) override {
|
||||
HKEY h_sub_key;
|
||||
LSTATUS status =
|
||||
RegOpenKeyExA(key, sub_key.data(), /*ulOptions=*/0,
|
||||
KEY_QUERY_VALUE | KEY_WOW64_32KEY, &h_sub_key);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
LOG(ERROR) << "Failed to open key: " << sub_key << ", status: " << status;
|
||||
return status;
|
||||
}
|
||||
DWORD index = 0;
|
||||
std::string value_name;
|
||||
value_name.reserve(500);
|
||||
do {
|
||||
DWORD type = 0;
|
||||
DWORD name_size = value_name.capacity();
|
||||
DWORD value_size = 0;
|
||||
status = RegEnumValueA(h_sub_key, index, value_name.data(), &name_size,
|
||||
/*lpReserved=*/nullptr, &type, /*lpData=*/nullptr,
|
||||
&value_size);
|
||||
if (status != ERROR_SUCCESS && status != ERROR_MORE_DATA) {
|
||||
break;
|
||||
}
|
||||
if (name_size >= value_name.capacity()) {
|
||||
value_name.reserve(name_size + 1);
|
||||
}
|
||||
name_size = value_name.capacity();
|
||||
if (type == REG_SZ) {
|
||||
std::string value(value_size, '\0');
|
||||
value_size = value.size();
|
||||
status =
|
||||
RegEnumValueA(h_sub_key, index, value_name.data(), &name_size,
|
||||
/*lpReserved=*/nullptr, &type,
|
||||
reinterpret_cast<LPBYTE>(value.data()), &value_size);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
break;
|
||||
}
|
||||
// value_size may or may not include null terminator. If it does we
|
||||
// need to remove it from the string.
|
||||
if (value_size > 0 && value[value_size - 1] == '\0') {
|
||||
value.resize(value_size - 1);
|
||||
}
|
||||
string_values.emplace(std::string(value_name.data(), name_size),
|
||||
std::move(value));
|
||||
} else if (type == REG_DWORD) {
|
||||
DWORD value = 0;
|
||||
value_size = sizeof(DWORD);
|
||||
status = RegEnumValueA(h_sub_key, index, value_name.data(), &name_size,
|
||||
/*lpReserved=*/nullptr, &type,
|
||||
reinterpret_cast<LPBYTE>(&value), &value_size);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
break;
|
||||
}
|
||||
dword_values.emplace(std::string(value_name.data(), name_size), value);
|
||||
}
|
||||
++index;
|
||||
} while (status == ERROR_SUCCESS);
|
||||
RegCloseKey(h_sub_key);
|
||||
if (status == ERROR_NO_MORE_ITEMS) {
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<std::string> GetKey(Registry::Key key,
|
||||
absl::string_view product_id) {
|
||||
switch (key) {
|
||||
case Registry::Key::kClients:
|
||||
return absl::StrCat(kGoogleUpdateClientsKey, product_id);
|
||||
case Registry::Key::kClientState:
|
||||
return absl::StrCat(kGoogleUpdateClientStateKey, product_id);
|
||||
case Registry::Key::kClientStateMedium:
|
||||
return absl::StrCat(kGoogleUpdateClientStateMediumKey, product_id);
|
||||
case Registry::Key::kNearbyShare:
|
||||
return std::string(kGoogleNearbyShareKey);
|
||||
case Registry::Key::kNearbyShareFlags:
|
||||
return std::string(kGoogleNearbyShareFlagsKey);
|
||||
case Registry::Key::kWindows:
|
||||
return std::string(kWindowsKey);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<HKEY> GetHiveKey(Registry::Hive hive) {
|
||||
switch (hive) {
|
||||
case Registry::Hive::kCurrentConfig:
|
||||
return HKEY_CURRENT_CONFIG;
|
||||
case Registry::Hive::kCurrentUser:
|
||||
return HKEY_CURRENT_USER;
|
||||
case Registry::Hive::kSoftware:
|
||||
case Registry::Hive::kSystem:
|
||||
return HKEY_LOCAL_MACHINE;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
absl::string_view GetHiveRoot(Registry::Hive hive) {
|
||||
switch (hive) {
|
||||
case Registry::Hive::kCurrentConfig:
|
||||
case Registry::Hive::kCurrentUser:
|
||||
return kHiveRootSoftware;
|
||||
case Registry::Hive::kSoftware:
|
||||
#ifndef _WIN64
|
||||
return kHiveRootSoftware;
|
||||
#else
|
||||
return kHiveRootSoftwareWow6432Node;
|
||||
#endif
|
||||
case Registry::Hive::kSystem:
|
||||
return kHiveRootSystem;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const char* GetHiveNameForLog(Registry::Hive hive) {
|
||||
switch (hive) {
|
||||
case Registry::Hive::kCurrentConfig:
|
||||
return "HKEY_CURRENT_CONFIG";
|
||||
case Registry::Hive::kCurrentUser:
|
||||
return "HKEY_CURRENT_USER";
|
||||
case Registry::Hive::kSoftware:
|
||||
case Registry::Hive::kSystem:
|
||||
return "HKEY_LOCAL_MACHINE";
|
||||
}
|
||||
|
||||
return "[REQUESTED HIVE OUT OF ALLOW LIST RANGE]";
|
||||
}
|
||||
|
||||
std::optional<std::pair<HKEY, std::string>> LookupRegistryPath(
|
||||
Registry::Hive hive, Registry::Key key, absl::string_view product_id) {
|
||||
std::optional<HKEY> hKey = GetHiveKey(hive);
|
||||
if (!hKey.has_value()) {
|
||||
LOG(ERROR) << "Requested registry hive is out of allow list range: "
|
||||
<< static_cast<int>(hive);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> keyPath = GetKey(key, product_id);
|
||||
if (!keyPath.has_value()) {
|
||||
LOG(ERROR) << "Requested registry key is out of allow list range: "
|
||||
<< static_cast<int>(key);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::make_pair(hKey.value(),
|
||||
absl::StrCat(GetHiveRoot(hive), "\\", keyPath.value()));
|
||||
}
|
||||
|
||||
// WindowsRegistryAccessor has trivial destructor.
|
||||
WindowsRegistryAccessor windows_accessor;
|
||||
RegistryAccessor* test_accessor = nullptr;
|
||||
|
||||
RegistryAccessor* getRegistryAccessor() {
|
||||
if (test_accessor == nullptr) {
|
||||
return &windows_accessor;
|
||||
}
|
||||
return test_accessor;
|
||||
}
|
||||
|
||||
absl::string_view (*product_id_func)() = nullptr;
|
||||
|
||||
absl::string_view GetProductId() {
|
||||
if (product_id_func == nullptr) {
|
||||
return kQuickShareAppProductId;
|
||||
}
|
||||
return product_id_func();
|
||||
}
|
||||
|
||||
#define REG_LOG(severity, message, hive, key, val, pretty_name, status) \
|
||||
LOG(severity) << message << " " << GetHiveNameForLog(hive) << "\\" << key \
|
||||
<< "\\" << val \
|
||||
<< (pretty_name.has_value() \
|
||||
? " (" + std::string(pretty_name.value()) + ")" \
|
||||
: "") \
|
||||
<< ", status: " << status;
|
||||
} // namespace
|
||||
|
||||
std::optional<uint32_t> Registry::ReadDword(
|
||||
Registry::Hive hive, Registry::Key key, const std::string& value,
|
||||
std::optional<absl::string_view> pretty_name) {
|
||||
DWORD result = 0;
|
||||
|
||||
std::optional<std::pair<HKEY, std::string>> path =
|
||||
LookupRegistryPath(hive, key, GetProductId());
|
||||
if (!path.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HKEY hKey = path.value().first;
|
||||
const std::string& qualified_key = path.value().second;
|
||||
|
||||
LSTATUS status =
|
||||
getRegistryAccessor()->ReadDWordValue(hKey, qualified_key, value, result);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
REG_LOG(WARNING, "Unable to read registry value", hive, qualified_key,
|
||||
value, pretty_name, status);
|
||||
return std::nullopt;
|
||||
}
|
||||
REG_LOG(INFO, "Successfully read registry value", hive, qualified_key, value,
|
||||
pretty_name, status);
|
||||
return static_cast<uint32_t>(result);
|
||||
}
|
||||
|
||||
std::optional<std::string> Registry::ReadString(
|
||||
Registry::Hive hive, Registry::Key key, const std::string& value,
|
||||
std::optional<absl::string_view> pretty_name) {
|
||||
std::optional<std::tuple<HKEY, std::string>> path =
|
||||
LookupRegistryPath(hive, key, GetProductId());
|
||||
if (!path.has_value()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
HKEY hKey = std::get<0>(path.value());
|
||||
const std::string& qualified_key = std::get<1>(path.value());
|
||||
|
||||
std::string result;
|
||||
LSTATUS status = getRegistryAccessor()->ReadStringValue(hKey, qualified_key,
|
||||
value, result);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
REG_LOG(WARNING, "Unable to read registry value", hive, qualified_key,
|
||||
value, pretty_name, status);
|
||||
return std::nullopt;
|
||||
}
|
||||
REG_LOG(INFO, "Successfully read registry value", hive, qualified_key, value,
|
||||
pretty_name, status);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Registry::SetDword(Registry::Hive hive, Registry::Key key,
|
||||
const std::string& value, uint32_t data,
|
||||
bool create_sub_key) {
|
||||
std::optional<std::tuple<HKEY, std::string>> path =
|
||||
LookupRegistryPath(hive, key, GetProductId());
|
||||
if (!path.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HKEY hKey = std::get<0>(path.value());
|
||||
const std::string& qualified_key = std::get<1>(path.value());
|
||||
|
||||
LSTATUS status = getRegistryAccessor()->WriteDWordValue(
|
||||
hKey, qualified_key, value, data, create_sub_key);
|
||||
std::optional<absl::string_view> empty = std::nullopt;
|
||||
if (status == ERROR_SUCCESS) {
|
||||
REG_LOG(INFO, "Successfully wrote " << data << " to", hive, qualified_key,
|
||||
value, empty, status);
|
||||
return true;
|
||||
}
|
||||
REG_LOG(WARNING, "Failed to write " << data << " to", hive, qualified_key,
|
||||
value, empty, status);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Registry::SetString(Hive hive, Key key, const std::string& value,
|
||||
const std::string& data, bool create_sub_key) {
|
||||
std::optional<std::tuple<HKEY, std::string>> path =
|
||||
LookupRegistryPath(hive, key, GetProductId());
|
||||
if (!path.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HKEY hKey = std::get<0>(path.value());
|
||||
const std::string& qualified_key = std::get<1>(path.value());
|
||||
|
||||
LSTATUS status = getRegistryAccessor()->WriteStringValue(
|
||||
hKey, qualified_key, value, data, create_sub_key);
|
||||
std::optional<absl::string_view> empty = std::nullopt;
|
||||
if (status == ERROR_SUCCESS) {
|
||||
REG_LOG(INFO, "Successfully wrote " << data << " to", hive, qualified_key,
|
||||
value, empty, status);
|
||||
return true;
|
||||
}
|
||||
REG_LOG(WARNING, "Failed to write " << data << " to", hive, qualified_key,
|
||||
value, empty, status);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Registry::EnumValues(
|
||||
Hive hive, Key key,
|
||||
absl::flat_hash_map<std::string, std::string>& string_values,
|
||||
absl::flat_hash_map<std::string, DWORD>& dword_values) {
|
||||
std::optional<std::tuple<HKEY, std::string>> path =
|
||||
LookupRegistryPath(hive, key, GetProductId());
|
||||
if (!path.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HKEY hKey = std::get<0>(path.value());
|
||||
const std::string& qualified_key = std::get<1>(path.value());
|
||||
|
||||
LSTATUS status = getRegistryAccessor()->EnumValues(
|
||||
hKey, qualified_key, string_values, dword_values);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
REG_LOG(WARNING, "Failed to enumerate values", hive, qualified_key,
|
||||
/*value=*/"", /*pretty_name=*/std::optional<absl::string_view>(),
|
||||
status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Registry::SetRegistryAccessorForTest(RegistryAccessor* registry_accessor) {
|
||||
test_accessor = registry_accessor;
|
||||
}
|
||||
|
||||
void Registry::SetProductIdGetter(absl::string_view (*product_id_getter)()) {
|
||||
product_id_func = product_id_getter;
|
||||
}
|
||||
|
||||
} // namespace nearby::platform::windows
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_REGISTRY_H_
|
||||
#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_REGISTRY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "internal/platform/implementation/windows/registry_accessor.h"
|
||||
|
||||
namespace nearby::platform::windows {
|
||||
|
||||
class Registry {
|
||||
public:
|
||||
enum class Hive {
|
||||
kCurrentConfig,
|
||||
kCurrentUser,
|
||||
|
||||
// Local machine hives
|
||||
// kSAM, Blocked
|
||||
// kSecurity, Blocked
|
||||
kSoftware,
|
||||
kSystem
|
||||
};
|
||||
|
||||
enum class Key {
|
||||
kClients, // Read from Google\Update\Clients.
|
||||
kClientState, // Read from Google\Update\ClientState.
|
||||
kClientStateMedium, // Read from Google\Update\ClientStateMedium.
|
||||
kNearbyShare, // Read from Google\NearbyShare.
|
||||
kNearbyShareFlags, // Read from Google\NearbyShare\Flags.
|
||||
kWindows // Read from Microsoft\Windows NT\CurrentVersion, for testing.
|
||||
};
|
||||
|
||||
static std::optional<uint32_t> ReadDword(
|
||||
Hive hive, Key key, const std::string& value,
|
||||
std::optional<absl::string_view> pretty_name = std::nullopt);
|
||||
static std::optional<std::string> ReadString(
|
||||
Hive hive, Key key, const std::string& value,
|
||||
std::optional<absl::string_view> pretty_name = std::nullopt);
|
||||
|
||||
// If |create_sub_key| is true, the sub_key will be created if it does not
|
||||
// exist.
|
||||
// NOTE: do not set create_sub_key to true unless you know the current user
|
||||
// has the permission to create the sub key otherwise elevation will be
|
||||
// required.
|
||||
static bool SetDword(Hive hive, Key key, const std::string& value,
|
||||
uint32_t data, bool create_sub_key = false);
|
||||
// If |create_sub_key| is true, the sub_key will be created if it does not
|
||||
// exist.
|
||||
// NOTE: do not set create_sub_key to true unless you know the current user
|
||||
// has the permission to create the sub key otherwise elevation will be
|
||||
// required.
|
||||
static bool SetString(Hive hive, Key key, const std::string& value,
|
||||
const std::string& data, bool create_sub_key = false);
|
||||
|
||||
// Enumerate all string and dword values in the registry key.
|
||||
static bool EnumValues(
|
||||
Hive hive, Key key,
|
||||
absl::flat_hash_map<std::string, std::string>& string_values,
|
||||
absl::flat_hash_map<std::string, DWORD>& dword_values);
|
||||
|
||||
// Use provided RegistryAccessor for testing.
|
||||
static void SetRegistryAccessorForTest(RegistryAccessor* registry_accessor);
|
||||
|
||||
// Set a function that is used to provide a Omaha product Id.
|
||||
static void SetProductIdGetter(absl::string_view (*product_id_getter)());
|
||||
};
|
||||
|
||||
} // namespace nearby::platform::windows
|
||||
|
||||
#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_REGISTRY_H_
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_REGISTRY_ACCESSOR_H_
|
||||
#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_REGISTRY_ACCESSOR_H_
|
||||
|
||||
#define _WIN32_WINNT _WIN32_WINNT_WIN10
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
namespace nearby::platform::windows {
|
||||
|
||||
// Interface wrapping windows registry access to facilitate testing.
|
||||
class RegistryAccessor {
|
||||
public:
|
||||
virtual ~RegistryAccessor() = default;
|
||||
|
||||
virtual LSTATUS ReadDWordValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name,
|
||||
DWORD& value_data) = 0;
|
||||
virtual LSTATUS ReadStringValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name,
|
||||
std::string& value_data) = 0;
|
||||
// If |create_sub_key| is true, the sub_key will be created if it does not
|
||||
// exist.
|
||||
virtual LSTATUS WriteDWordValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name,
|
||||
DWORD value_data, bool create_sub_key) = 0;
|
||||
// If |create_sub_key| is true, the sub_key will be created if it does not
|
||||
// exist.
|
||||
virtual LSTATUS WriteStringValue(HKEY key, const std::string& sub_key,
|
||||
const std::string& value_name,
|
||||
const std::string& value_data,
|
||||
bool create_sub_key) = 0;
|
||||
|
||||
virtual LSTATUS EnumValues(
|
||||
HKEY key, const std::string& sub_key,
|
||||
absl::flat_hash_map<std::string, std::string>& string_values,
|
||||
absl::flat_hash_map<std::string, DWORD>& dword_values) = 0;
|
||||
};
|
||||
|
||||
} // namespace nearby::platform::windows
|
||||
|
||||
#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_IMPL_WINDOWS_REGISTRY_ACCESSOR_H_
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#include "internal/platform/implementation/windows/registry.h"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "protobuf-matchers/protocol-buffer-matchers.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "internal/platform/implementation/windows/mock_registry_accessor.h"
|
||||
|
||||
namespace nearby::platform::windows {
|
||||
namespace {
|
||||
using ::testing::_;
|
||||
using ::testing::Eq;
|
||||
using ::testing::IsEmpty;
|
||||
using ::testing::IsFalse;
|
||||
using ::testing::IsTrue;
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::Not;
|
||||
using ::testing::Return;
|
||||
|
||||
constexpr absl::string_view kClientsRegKeyPath =
|
||||
R"(SOFTWARE\Google\Update\Clients\{232066FE-FF4D-4C25-83B4-3F8747CF7E3A})";
|
||||
constexpr absl::string_view kClientStateRegKeyPath =
|
||||
R"(SOFTWARE\Google\Update\ClientState\{232066FE-FF4D-4C25-83B4-3F8747CF7E3A})";
|
||||
constexpr absl::string_view kClientStateMediumRegKeyPath =
|
||||
R"(SOFTWARE\Google\Update\ClientStateMedium\{232066FE-FF4D-4C25-83B4-3F8747CF7E3A})";
|
||||
|
||||
constexpr absl::string_view kTestProductId = "{test_product_id}";
|
||||
constexpr absl::string_view kTestClientsRegKeyPath =
|
||||
R"(SOFTWARE\Google\Update\Clients\{test_product_id})";
|
||||
constexpr absl::string_view kTestClientStateRegKeyPath =
|
||||
R"(SOFTWARE\Google\Update\ClientState\{test_product_id})";
|
||||
constexpr absl::string_view kTestClientStateMediumRegKeyPath =
|
||||
R"(SOFTWARE\Google\Update\ClientStateMedium\{test_product_id})";
|
||||
|
||||
absl::string_view GetTestProductId() { return kTestProductId; }
|
||||
|
||||
// Enable only for local runs that have the client state key created
|
||||
// TODO(mattkaes): create the entire registry key chain if it's missing
|
||||
TEST(DISABLED_Registry, CreateAndRead) {
|
||||
const int test_level = 4;
|
||||
|
||||
bool set_success =
|
||||
Registry::SetDword(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kClientState, "log_level", test_level);
|
||||
ASSERT_TRUE(set_success);
|
||||
|
||||
auto log_level = Registry::ReadDword(
|
||||
Registry::Hive::kCurrentUser, Registry::Key::kClientState, "log_level");
|
||||
ASSERT_TRUE(log_level.has_value());
|
||||
EXPECT_EQ(test_level, log_level.value());
|
||||
}
|
||||
|
||||
TEST(Registry, ReadDwordNonexistentValue) {
|
||||
auto reg_value = Registry::ReadDword(
|
||||
Registry::Hive::kCurrentUser, Registry::Key::kClientState,
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1");
|
||||
EXPECT_THAT(reg_value.has_value(), IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, BlockBadHiveReadDword) {
|
||||
auto reg_value = Registry::ReadDword(
|
||||
static_cast<Registry::Hive>(891), Registry::Key::kClientState,
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1");
|
||||
EXPECT_THAT(reg_value.has_value(), IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, BlockBadKeyReadDword) {
|
||||
auto reg_value = Registry::ReadDword(
|
||||
Registry::Hive::kCurrentUser, static_cast<Registry::Key>(891),
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1");
|
||||
EXPECT_THAT(reg_value.has_value(), IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, ReadStringNonexistentValue) {
|
||||
auto reg_value = Registry::ReadString(
|
||||
Registry::Hive::kCurrentUser, Registry::Key::kNearbyShare,
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1");
|
||||
EXPECT_THAT(reg_value.has_value(), IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, BlockBadHiveReadString) {
|
||||
auto reg_value = Registry::ReadString(
|
||||
static_cast<Registry::Hive>(891), Registry::Key::kClientState,
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1");
|
||||
EXPECT_THAT(reg_value.has_value(), IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, BlockBadKeyReadString) {
|
||||
auto reg_value = Registry::ReadString(
|
||||
Registry::Hive::kCurrentUser, static_cast<Registry::Key>(891),
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1");
|
||||
EXPECT_THAT(reg_value.has_value(), IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, BlockBadHiveWrite) {
|
||||
auto success = Registry::SetDword(
|
||||
static_cast<Registry::Hive>(891), Registry::Key::kClientState,
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1", 891);
|
||||
EXPECT_THAT(success, IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, BlockBadKeyWrite) {
|
||||
auto success = Registry::SetDword(
|
||||
Registry::Hive::kCurrentUser, static_cast<Registry::Key>(891),
|
||||
"NA-bc1adb34-493f-4952-8ce6-bcf2acddfbe1", 891);
|
||||
EXPECT_THAT(success, IsFalse());
|
||||
}
|
||||
|
||||
TEST(Registry, ReadStringSucceeds) {
|
||||
std::optional<std::string> result = Registry::ReadString(
|
||||
Registry::Hive::kSoftware, Registry::Key::kWindows, "ProductName");
|
||||
EXPECT_THAT(result.has_value(), IsTrue());
|
||||
EXPECT_THAT(result.value(), Not(IsEmpty()));
|
||||
}
|
||||
|
||||
TEST(Registry, ReadDWORDSucceeds) {
|
||||
std::optional<unsigned> result =
|
||||
Registry::ReadDword(Registry::Hive::kSoftware, Registry::Key::kWindows,
|
||||
"CurrentMajorVersionNumber");
|
||||
EXPECT_THAT(result.has_value(), IsTrue());
|
||||
EXPECT_THAT(result.value(), Not(Eq(0)));
|
||||
}
|
||||
|
||||
// Enable only for local runs that have the client state key created
|
||||
// This test requires you run with Admin privileges in order to write to
|
||||
// HKEY_LOCAL_MACHINE.
|
||||
TEST(Registry, DISABLED_ValidateKeyWrites) {
|
||||
const int test_value = 10;
|
||||
|
||||
const std::pair<platform::windows::Registry::Hive,
|
||||
platform::windows::Registry::Key>
|
||||
test_hives[] = {{
|
||||
platform::windows::Registry::Hive::kCurrentUser,
|
||||
platform::windows::Registry::Key::kClientState,
|
||||
},
|
||||
{
|
||||
platform::windows::Registry::Hive::kCurrentUser,
|
||||
platform::windows::Registry::Key::kClients,
|
||||
},
|
||||
{
|
||||
platform::windows::Registry::Hive::kSoftware,
|
||||
platform::windows::Registry::Key::kClients,
|
||||
}};
|
||||
|
||||
for (auto& key_set : test_hives) {
|
||||
auto success = platform::windows::Registry::SetDword(
|
||||
key_set.first, key_set.second, "test_key", test_value);
|
||||
EXPECT_THAT(success, IsTrue());
|
||||
if (!success) {
|
||||
std::cout << "failed to write to hive=" << static_cast<int>(key_set.first)
|
||||
<< ", key=" << static_cast<int>(key_set.second) << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Registry, ValidateReadClientState) {
|
||||
auto mock_registry_accessor =
|
||||
std::make_unique<NiceMock<MockRegistryAccessor>>();
|
||||
EXPECT_CALL(*mock_registry_accessor,
|
||||
ReadDWordValue(_, std::string(kClientStateRegKeyPath), _, _))
|
||||
.WillOnce(Return(ERROR_SUCCESS));
|
||||
Registry::SetRegistryAccessorForTest(mock_registry_accessor.get());
|
||||
(void)Registry::ReadDword(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kClientState, "log_level");
|
||||
}
|
||||
|
||||
TEST(Registry, ValidateReadClients) {
|
||||
auto mock_registry_accessor =
|
||||
std::make_unique<NiceMock<MockRegistryAccessor>>();
|
||||
EXPECT_CALL(*mock_registry_accessor,
|
||||
ReadStringValue(_, std::string(kClientsRegKeyPath), _, _))
|
||||
.WillOnce(Return(ERROR_SUCCESS));
|
||||
Registry::SetRegistryAccessorForTest(mock_registry_accessor.get());
|
||||
std::optional<std::string> result = Registry::ReadString(
|
||||
Registry::Hive::kCurrentUser, Registry::Key::kClients, "log_level");
|
||||
}
|
||||
|
||||
TEST(Registry, ValidateReadClientStateMedium) {
|
||||
auto mock_registry_accessor =
|
||||
std::make_unique<NiceMock<MockRegistryAccessor>>();
|
||||
EXPECT_CALL(
|
||||
*mock_registry_accessor,
|
||||
ReadStringValue(_, std::string(kClientStateMediumRegKeyPath), _, _))
|
||||
.WillOnce(Return(ERROR_SUCCESS));
|
||||
Registry::SetRegistryAccessorForTest(mock_registry_accessor.get());
|
||||
std::optional<std::string> result =
|
||||
Registry::ReadString(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kClientStateMedium, "log_level");
|
||||
}
|
||||
|
||||
TEST(Registry, ValidateReadClientStateWithProductIdOverride) {
|
||||
auto mock_registry_accessor =
|
||||
std::make_unique<NiceMock<MockRegistryAccessor>>();
|
||||
EXPECT_CALL(*mock_registry_accessor,
|
||||
ReadDWordValue(_, std::string(kTestClientStateRegKeyPath), _, _))
|
||||
.WillOnce(Return(ERROR_SUCCESS));
|
||||
Registry::SetProductIdGetter(GetTestProductId);
|
||||
Registry::SetRegistryAccessorForTest(mock_registry_accessor.get());
|
||||
(void)Registry::ReadDword(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kClientState, "log_level");
|
||||
}
|
||||
|
||||
TEST(Registry, ValidateReadClientsWithProductIdOverride) {
|
||||
auto mock_registry_accessor =
|
||||
std::make_unique<NiceMock<MockRegistryAccessor>>();
|
||||
EXPECT_CALL(*mock_registry_accessor,
|
||||
ReadStringValue(_, std::string(kTestClientsRegKeyPath), _, _))
|
||||
.WillOnce(Return(ERROR_SUCCESS));
|
||||
Registry::SetProductIdGetter(GetTestProductId);
|
||||
Registry::SetRegistryAccessorForTest(mock_registry_accessor.get());
|
||||
std::optional<std::string> result = Registry::ReadString(
|
||||
Registry::Hive::kCurrentUser, Registry::Key::kClients, "log_level");
|
||||
}
|
||||
|
||||
TEST(Registry, ValidateReadClientStateMediumWithProductIdOverride) {
|
||||
auto mock_registry_accessor =
|
||||
std::make_unique<NiceMock<MockRegistryAccessor>>();
|
||||
EXPECT_CALL(
|
||||
*mock_registry_accessor,
|
||||
ReadStringValue(_, std::string(kTestClientStateMediumRegKeyPath), _, _))
|
||||
.WillOnce(Return(ERROR_SUCCESS));
|
||||
Registry::SetProductIdGetter(GetTestProductId);
|
||||
Registry::SetRegistryAccessorForTest(mock_registry_accessor.get());
|
||||
std::optional<std::string> result =
|
||||
Registry::ReadString(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kClientStateMedium, "log_level");
|
||||
}
|
||||
|
||||
TEST(Registry, ValidateEnumValues) {
|
||||
Registry::SetRegistryAccessorForTest(nullptr);
|
||||
EXPECT_TRUE(Registry::SetDword(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kNearbyShareFlags, "12345",
|
||||
12345, /*create_sub_key=*/true));
|
||||
EXPECT_TRUE(Registry::SetString(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kNearbyShareFlags, "98786",
|
||||
"9876", /*create_sub_key=*/true));
|
||||
absl::flat_hash_map<std::string, std::string> string_values;
|
||||
absl::flat_hash_map<std::string, DWORD> dword_values;
|
||||
Registry::EnumValues(Registry::Hive::kCurrentUser,
|
||||
Registry::Key::kNearbyShareFlags, string_values,
|
||||
dword_values);
|
||||
EXPECT_THAT(string_values.size(), Eq(1));
|
||||
EXPECT_THAT(dword_values.size(), Eq(1));
|
||||
EXPECT_THAT(string_values["98786"], Eq("9876"));
|
||||
EXPECT_THAT(dword_values["12345"], Eq(12345));
|
||||
::RegDeleteKeyA(HKEY_CURRENT_USER, "Google\\NearbyShare\\Flags");
|
||||
}
|
||||
} // namespace
|
||||
} // namespace nearby::platform::windows
|
||||
Reference in New Issue
Block a user