mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-11-01 17:46:42 +09:00
364 lines
15 KiB
C++
364 lines
15 KiB
C++
/**
|
|
* @file tests/unit/test_windows_driver_protocol.cpp
|
|
* @brief Unit tests for private Windows driver identity and controller protocol helpers.
|
|
*/
|
|
|
|
// local includes
|
|
#include "fixtures/fixtures.hpp"
|
|
#include "playstation_feature_protocol.hpp"
|
|
#include "shared/switch_pro_protocol.hpp"
|
|
#include "vhf_report_response.hpp"
|
|
#include "windows_device_identity.hpp"
|
|
|
|
// standard includes
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <span>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
class WindowsDriverProtocolTest: public WindowsTest {};
|
|
|
|
std::vector<std::wstring> hardware_id_entries(const std::wstring &hardware_ids) {
|
|
std::vector<std::wstring> entries;
|
|
auto offset = std::size_t {};
|
|
while (offset < hardware_ids.size() && hardware_ids[offset] != L'\0') {
|
|
const auto end = hardware_ids.find(L'\0', offset);
|
|
entries.emplace_back(hardware_ids.substr(offset, end - offset));
|
|
offset = end + 1U;
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
LvhWindowsCreateDeviceRequest series_request() {
|
|
LvhWindowsCreateDeviceRequest request {};
|
|
request.device_type = LVH_WINDOWS_DEVICE_GAMEPAD;
|
|
request.gamepad_kind = LVH_WINDOWS_GAMEPAD_XBOX_SERIES;
|
|
request.hardware_ids.vendor_id = 0x045E;
|
|
request.hardware_ids.product_id = 0x0B12;
|
|
request.hardware_ids.device_version = 0x0500;
|
|
return request;
|
|
}
|
|
|
|
LvhWindowsCreateDeviceRequest playstation_request(
|
|
std::uint32_t gamepad_kind,
|
|
std::uint32_t bus_type = LVH_WINDOWS_BUS_USB,
|
|
std::string_view stable_id = "10:20:30:40:50:60"
|
|
) {
|
|
LvhWindowsCreateDeviceRequest request {};
|
|
request.device_type = LVH_WINDOWS_DEVICE_GAMEPAD;
|
|
request.client_device_id = 0x12345678U;
|
|
request.gamepad_kind = gamepad_kind;
|
|
request.bus_type = bus_type;
|
|
request.report_sizes.stable_id_size = static_cast<std::uint32_t>(stable_id.size());
|
|
std::ranges::copy(stable_id, request.stable_id.begin());
|
|
return request;
|
|
}
|
|
|
|
std::array<std::uint8_t, lvh::detail::switch_pro_protocol::switch_pro_report_size> switch_output_report(
|
|
std::uint8_t report_id,
|
|
std::uint8_t command
|
|
) {
|
|
std::array<std::uint8_t, lvh::detail::switch_pro_protocol::switch_pro_report_size> report {};
|
|
report[0] = report_id;
|
|
if (report_id == 0x80U) {
|
|
report[1] = command;
|
|
} else {
|
|
report[1] = 0x07;
|
|
report[10] = command;
|
|
}
|
|
return report;
|
|
}
|
|
|
|
void write_u32(std::span<std::uint8_t> report, std::size_t offset, std::uint32_t value) {
|
|
report[offset] = static_cast<std::uint8_t>(value & 0xFFU);
|
|
report[offset + 1U] = static_cast<std::uint8_t>((value >> 8U) & 0xFFU);
|
|
report[offset + 2U] = static_cast<std::uint8_t>((value >> 16U) & 0xFFU);
|
|
report[offset + 3U] = static_cast<std::uint8_t>((value >> 24U) & 0xFFU);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_F(WindowsDriverProtocolTest, VhfFeatureResponseDoesNotWriteToAdvertisedPadding) {
|
|
const auto request = playstation_request(LVH_WINDOWS_GAMEPAD_DUALSENSE);
|
|
for (const auto report_id : {0x05U, 0x09U, 0x20U}) {
|
|
SCOPED_TRACE(report_id);
|
|
const auto report = lvh::detail::windows::make_playstation_feature_report(request, report_id);
|
|
ASSERT_TRUE(report.has_value());
|
|
ASSERT_LE(report->size(), 64U);
|
|
|
|
// VHF allocates 64 bytes but advertises 128 in issue #199's PageHeap dump.
|
|
// Extra backing storage lets the test detect the overrun without invoking UB.
|
|
auto buffer = std::array<std::uint8_t, 128> {};
|
|
buffer.fill(0xA5);
|
|
ASSERT_TRUE(lvh::detail::windows::copy_vhf_report_response(buffer.data(), 128U, *report));
|
|
EXPECT_TRUE(std::ranges::equal(*report, std::span {buffer}.first(report->size())));
|
|
EXPECT_TRUE(std::ranges::all_of(std::span {buffer}.subspan(report->size()), [](auto byte) {
|
|
return byte == 0xA5;
|
|
}));
|
|
}
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, VhfInputResponseDoesNotWritePastTheReport) {
|
|
auto report = std::array<std::uint8_t, 64> {};
|
|
report.fill(0x5A);
|
|
report[0] = 0x01;
|
|
auto buffer = std::array<std::uint8_t, 128> {};
|
|
buffer.fill(0xA5);
|
|
|
|
ASSERT_TRUE(lvh::detail::windows::copy_vhf_report_response(buffer.data(), buffer.size(), report));
|
|
EXPECT_TRUE(std::ranges::equal(report, std::span {buffer}.first(report.size())));
|
|
EXPECT_TRUE(std::ranges::all_of(std::span {buffer}.subspan(report.size()), [](auto byte) {
|
|
return byte == 0xA5;
|
|
}));
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, VhfUnnumberedResponseWritesOnlyTheOptionalIdAndReport) {
|
|
const auto report = std::array<std::uint8_t, 8> {1, 2, 3, 4, 5, 6, 7, 8};
|
|
for (const auto prepend_zero_id : {false, true}) {
|
|
SCOPED_TRACE(prepend_zero_id);
|
|
const auto prefix_size = prepend_zero_id ? 1U : 0U;
|
|
auto buffer = std::array<std::uint8_t, 128> {};
|
|
buffer.fill(0xA5);
|
|
|
|
ASSERT_TRUE(lvh::detail::windows::copy_vhf_report_response(buffer.data(), buffer.size(), report, prepend_zero_id));
|
|
if (prepend_zero_id) {
|
|
EXPECT_EQ(buffer[0], 0U);
|
|
}
|
|
EXPECT_TRUE(std::ranges::equal(report, std::span {buffer}.subspan(prefix_size, report.size())));
|
|
EXPECT_TRUE(std::ranges::all_of(std::span {buffer}.subspan(prefix_size + report.size()), [](auto byte) {
|
|
return byte == 0xA5;
|
|
}));
|
|
}
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, VhfResponseRejectsShortRequestsWithoutWriting) {
|
|
const auto report = std::array<std::uint8_t, 8> {1, 2, 3, 4, 5, 6, 7, 8};
|
|
auto buffer = std::array<std::uint8_t, 16> {};
|
|
buffer.fill(0xA5);
|
|
EXPECT_FALSE(lvh::detail::windows::copy_vhf_report_response(buffer.data(), report.size() - 1U, report));
|
|
EXPECT_FALSE(lvh::detail::windows::copy_vhf_report_response(buffer.data(), report.size(), report, true));
|
|
EXPECT_TRUE(std::ranges::all_of(buffer, [](auto byte) {
|
|
return byte == 0xA5;
|
|
}));
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, VhfResponseAcceptsExactlySizedRequests) {
|
|
const auto report = std::array<std::uint8_t, 3> {1, 2, 3};
|
|
auto numbered_buffer = std::array<std::uint8_t, 3> {};
|
|
ASSERT_TRUE(lvh::detail::windows::copy_vhf_report_response(numbered_buffer.data(), numbered_buffer.size(), report));
|
|
EXPECT_EQ(numbered_buffer, report);
|
|
|
|
auto unnumbered_buffer = std::array<std::uint8_t, 4> {};
|
|
unnumbered_buffer.fill(0xA5);
|
|
ASSERT_TRUE(lvh::detail::windows::copy_vhf_report_response(unnumbered_buffer.data(), unnumbered_buffer.size(), report, true));
|
|
EXPECT_EQ(unnumbered_buffer, (std::array<std::uint8_t, 4> {0, 1, 2, 3}));
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, SeriesUsesCapturedXInputHidIdentity) {
|
|
auto request = series_request();
|
|
request.hardware_ids.product_id = 0x0B12;
|
|
request.hardware_ids.device_version = 0x0509;
|
|
|
|
const auto entries = hardware_id_entries(lvh::detail::windows::make_hardware_ids(request));
|
|
ASSERT_EQ(entries.size(), 8U);
|
|
EXPECT_TRUE(lvh::detail::windows::uses_xinputhid_match_id(request.gamepad_kind));
|
|
EXPECT_EQ(entries[0], L"HID\\VID_045E&PID_0B12&IG_00");
|
|
EXPECT_EQ(entries[1], L"HID\\VID_045E&PID_02FF&IG_00");
|
|
EXPECT_EQ(entries[2], L"HID\\VID_045E&PID_0B12&REV_0509");
|
|
EXPECT_EQ(entries[3], L"HID\\VID_045E&PID_0B12");
|
|
EXPECT_EQ(entries[4], L"HID\\VID_045E&UP:0001_U:0005");
|
|
EXPECT_EQ(entries[5], L"HID_DEVICE_SYSTEM_GAME");
|
|
EXPECT_EQ(entries[6], L"HID_DEVICE_UP:0001_U:0005");
|
|
EXPECT_EQ(entries[7], L"HID_DEVICE");
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, NonSeriesXboxUsesItsPublicIdentityForMatching) {
|
|
auto request = series_request();
|
|
request.gamepad_kind = LVH_WINDOWS_GAMEPAD_XBOX_ONE;
|
|
request.hardware_ids.product_id = 0x02EA;
|
|
request.hardware_ids.device_version = 0x0408;
|
|
|
|
const auto entries = hardware_id_entries(lvh::detail::windows::make_hardware_ids(request));
|
|
ASSERT_EQ(entries.size(), 3U);
|
|
EXPECT_TRUE(lvh::detail::windows::uses_xinputhid_match_id(request.gamepad_kind));
|
|
EXPECT_EQ(entries[0], L"HID\\VID_045E&PID_02EA&IG_00");
|
|
EXPECT_EQ(entries[1], L"HID\\VID_045E&PID_02EA&REV_0408");
|
|
EXPECT_EQ(entries[2], L"HID\\VID_045E&PID_02EA");
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, SwitchProRepliesToUsbStatusAndHandshakeCommands) {
|
|
auto status_report = switch_output_report(0x80, 0x01);
|
|
const auto status_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(status_report);
|
|
ASSERT_TRUE(status_reply.has_value());
|
|
EXPECT_EQ(status_reply->at(0), 0x81);
|
|
EXPECT_EQ(status_reply->at(1), 0x01);
|
|
EXPECT_EQ(status_reply->at(3), 0x03);
|
|
EXPECT_TRUE(std::ranges::any_of(status_reply->begin() + 4, status_reply->begin() + 10, [](auto value) {
|
|
return value != 0U;
|
|
}));
|
|
|
|
for (const auto command : {0x02U, 0x03U}) {
|
|
const auto report = switch_output_report(0x80, static_cast<std::uint8_t>(command));
|
|
const auto reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(report);
|
|
ASSERT_TRUE(reply.has_value());
|
|
EXPECT_EQ(reply->at(0), 0x81);
|
|
EXPECT_EQ(reply->at(1), command);
|
|
}
|
|
|
|
const auto force_usb = switch_output_report(0x80, 0x04);
|
|
EXPECT_FALSE(lvh::detail::switch_pro_protocol::make_switch_pro_reply(force_usb).has_value());
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, SwitchProAcknowledgesInitializationSubcommands) {
|
|
for (const auto subcommand : {0x03U, 0x30U, 0x38U, 0x40U, 0x41U, 0x48U}) {
|
|
const auto report = switch_output_report(0x01, static_cast<std::uint8_t>(subcommand));
|
|
const auto reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(report, 0x42U);
|
|
ASSERT_TRUE(reply.has_value());
|
|
EXPECT_EQ(reply->at(0), 0x21);
|
|
EXPECT_EQ(reply->at(1), 0x42);
|
|
EXPECT_EQ(reply->at(2), 0x81);
|
|
EXPECT_EQ(reply->at(13), 0x80);
|
|
EXPECT_EQ(reply->at(14), subcommand);
|
|
EXPECT_EQ(reply->at(6), 0x00);
|
|
EXPECT_EQ(reply->at(7), 0x08);
|
|
EXPECT_EQ(reply->at(8), 0x80);
|
|
EXPECT_EQ(reply->at(9), 0x00);
|
|
EXPECT_EQ(reply->at(10), 0x08);
|
|
EXPECT_EQ(reply->at(11), 0x80);
|
|
}
|
|
|
|
const auto rumble_only = switch_output_report(0x10, 0x00);
|
|
EXPECT_FALSE(lvh::detail::switch_pro_protocol::make_switch_pro_reply(rumble_only).has_value());
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, SwitchProReturnsDeviceInfoAndFactoryCalibration) {
|
|
auto device_info = switch_output_report(0x01, 0x02);
|
|
const auto device_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(device_info);
|
|
ASSERT_TRUE(device_reply.has_value());
|
|
EXPECT_EQ(device_reply->at(13), 0x82);
|
|
EXPECT_EQ(device_reply->at(14), 0x02);
|
|
EXPECT_EQ(device_reply->at(17), 0x03);
|
|
|
|
auto stick_read = switch_output_report(0x01, 0x10);
|
|
write_u32(stick_read, 11U, 0x603D);
|
|
stick_read[15] = 18U;
|
|
const auto stick_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(stick_read);
|
|
ASSERT_TRUE(stick_reply.has_value());
|
|
EXPECT_EQ(stick_reply->at(13), 0x90);
|
|
EXPECT_EQ(stick_reply->at(14), 0x10);
|
|
EXPECT_TRUE(std::equal(stick_read.begin() + 11, stick_read.begin() + 16, stick_reply->begin() + 15));
|
|
constexpr std::array<std::uint8_t, 18> expected_stick_calibration {
|
|
0xFF,
|
|
0xF7,
|
|
0x7F,
|
|
0x00,
|
|
0x08,
|
|
0x80,
|
|
0xFF,
|
|
0xF7,
|
|
0x7F,
|
|
0x00,
|
|
0x08,
|
|
0x80,
|
|
0xFF,
|
|
0xF7,
|
|
0x7F,
|
|
0xFF,
|
|
0xF7,
|
|
0x7F,
|
|
};
|
|
EXPECT_TRUE(std::equal(expected_stick_calibration.begin(), expected_stick_calibration.end(), stick_reply->begin() + 20));
|
|
|
|
auto imu_read = switch_output_report(0x01, 0x10);
|
|
write_u32(imu_read, 11U, 0x6020);
|
|
imu_read[15] = 24U;
|
|
const auto imu_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(imu_read);
|
|
ASSERT_TRUE(imu_reply.has_value());
|
|
EXPECT_TRUE(std::ranges::any_of(imu_reply->begin() + 20, imu_reply->begin() + 44, [](auto value) {
|
|
return value != 0U;
|
|
}));
|
|
|
|
auto user_read = switch_output_report(0x01, 0x10);
|
|
write_u32(user_read, 11U, 0x8010);
|
|
user_read[15] = 22U;
|
|
const auto user_reply = lvh::detail::switch_pro_protocol::make_switch_pro_reply(user_read);
|
|
ASSERT_TRUE(user_reply.has_value());
|
|
EXPECT_TRUE(std::ranges::all_of(user_reply->begin() + 20, user_reply->begin() + 42, [](auto value) {
|
|
return value == 0U;
|
|
}));
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, DualShock4ReturnsCalibrationPairingAndFirmwareFeatures) {
|
|
const auto request = playstation_request(LVH_WINDOWS_GAMEPAD_DUALSHOCK4);
|
|
|
|
const auto calibration = lvh::detail::windows::make_playstation_feature_report(request, 0x02);
|
|
ASSERT_TRUE(calibration.has_value());
|
|
ASSERT_EQ(calibration->size(), 37U);
|
|
EXPECT_EQ(calibration->front(), 0x02);
|
|
EXPECT_EQ(calibration->at(7), 0x10);
|
|
EXPECT_EQ(calibration->at(8), 0x27);
|
|
EXPECT_FALSE(lvh::detail::windows::make_playstation_feature_report(request, 0x05).has_value());
|
|
|
|
const auto pairing = lvh::detail::windows::make_playstation_feature_report(request, 0x12);
|
|
ASSERT_TRUE(pairing.has_value());
|
|
ASSERT_EQ(pairing->size(), 16U);
|
|
constexpr std::array<std::uint8_t, 6> expected_mac {0x60, 0x50, 0x40, 0x30, 0x20, 0x10};
|
|
EXPECT_TRUE(std::equal(expected_mac.begin(), expected_mac.end(), pairing->begin() + 1));
|
|
|
|
const auto firmware = lvh::detail::windows::make_playstation_feature_report(request, 0xA3);
|
|
ASSERT_TRUE(firmware.has_value());
|
|
EXPECT_EQ(firmware->size(), 49U);
|
|
EXPECT_EQ(firmware->front(), 0xA3);
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, DualSenseReturnsCalibrationPairingAndFirmwareFeatures) {
|
|
const auto request = playstation_request(LVH_WINDOWS_GAMEPAD_DUALSENSE);
|
|
|
|
const auto calibration = lvh::detail::windows::make_playstation_feature_report(request, 0x05);
|
|
ASSERT_TRUE(calibration.has_value());
|
|
EXPECT_EQ(calibration->size(), 41U);
|
|
EXPECT_EQ(calibration->front(), 0x05);
|
|
|
|
const auto pairing = lvh::detail::windows::make_playstation_feature_report(request, 0x09);
|
|
ASSERT_TRUE(pairing.has_value());
|
|
ASSERT_EQ(pairing->size(), 20U);
|
|
constexpr std::array<std::uint8_t, 6> expected_mac {0x60, 0x50, 0x40, 0x30, 0x20, 0x10};
|
|
EXPECT_TRUE(std::equal(expected_mac.begin(), expected_mac.end(), pairing->begin() + 1));
|
|
|
|
const auto firmware = lvh::detail::windows::make_playstation_feature_report(request, 0x20);
|
|
ASSERT_TRUE(firmware.has_value());
|
|
EXPECT_EQ(firmware->size(), 64U);
|
|
EXPECT_EQ(firmware->front(), 0x20);
|
|
EXPECT_EQ(firmware->at(22), 0x04);
|
|
EXPECT_EQ(firmware->at(23), 0x00);
|
|
EXPECT_EQ(firmware->at(44), 0x30);
|
|
EXPECT_EQ(firmware->at(45), 0x06);
|
|
EXPECT_FALSE(lvh::detail::windows::make_playstation_feature_report(request, 0xFF).has_value());
|
|
}
|
|
|
|
TEST_F(WindowsDriverProtocolTest, BluetoothPlayStationFeaturesCarryCrcAndGeneratedPairingMac) {
|
|
auto request = playstation_request(LVH_WINDOWS_GAMEPAD_DUALSENSE, LVH_WINDOWS_BUS_BLUETOOTH, "not-a-mac");
|
|
|
|
const auto pairing = lvh::detail::windows::make_playstation_feature_report(request, 0x09);
|
|
ASSERT_TRUE(pairing.has_value());
|
|
ASSERT_EQ(pairing->size(), 20U);
|
|
constexpr std::array<std::uint8_t, 6> expected_generated_mac {0x78, 0x56, 0x34, 0x12, 0x00, 0x02};
|
|
EXPECT_TRUE(std::equal(expected_generated_mac.begin(), expected_generated_mac.end(), pairing->begin() + 1));
|
|
EXPECT_TRUE(std::ranges::any_of(pairing->end() - 4, pairing->end(), [](auto value) {
|
|
return value != 0U;
|
|
}));
|
|
|
|
request.gamepad_kind = LVH_WINDOWS_GAMEPAD_DUALSHOCK4;
|
|
const auto bluetooth_calibration = lvh::detail::windows::make_playstation_feature_report(request, 0x05);
|
|
ASSERT_TRUE(bluetooth_calibration.has_value());
|
|
EXPECT_EQ(bluetooth_calibration->size(), 41U);
|
|
EXPECT_TRUE(std::ranges::any_of(bluetooth_calibration->end() - 4, bluetooth_calibration->end(), [](auto value) {
|
|
return value != 0U;
|
|
}));
|
|
}
|