/** * @file tests/fixtures/linux_backend_test_hooks.cpp * @brief Linux backend test hook definitions. */ // standard includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // platform includes #if defined(__linux__) #include #ifndef __user #define __user #endif #include #include #include #include #include #include #include #if defined(LIBVIRTUALHID_HAVE_XTEST) #include #include #include #include #endif #endif // lib includes #if defined(__linux__) #include #include #endif // local includes #include "fixtures/linux_backend_test_hooks.hpp" #if defined(__linux__) namespace lvh::detail::test { namespace { struct FakeLibevdevDevice { std::string name; std::uint16_t bustype = 0; std::uint16_t vendor = 0; std::uint16_t product = 0; std::uint16_t version = 0; std::vector event_types; std::vector event_codes; std::vector properties; }; struct FakeLibevdevUinput { FakeLibevdevDevice device; }; template Target opaque_test_handle(Source *source) noexcept { static_assert(sizeof(Target) == sizeof(source), "opaque test handles must preserve pointer size"); return std::bit_cast(source); } libevdev *libevdev_handle(FakeLibevdevDevice *device) noexcept { return opaque_test_handle(device); } FakeLibevdevDevice *fake_libevdev_device(libevdev *device) noexcept { return opaque_test_handle(device); } const FakeLibevdevDevice *fake_libevdev_device(const libevdev *device) noexcept { return opaque_test_handle(device); } libevdev_uinput *libevdev_uinput_handle(FakeLibevdevUinput *device) noexcept { return opaque_test_handle(device); } FakeLibevdevUinput *fake_libevdev_uinput(libevdev_uinput *device) noexcept { return opaque_test_handle(device); } #if defined(LIBVIRTUALHID_HAVE_XTEST) Display *fake_display_handle() noexcept { static std::byte display_handle {}; return opaque_test_handle(&display_handle); } #endif struct LinuxTestSyscalls { // NOSONAR(cpp:S1820): Test-only fake syscall state keeps related Linux hook controls visible at call sites. bool override_access = false; int access_result = 0; bool override_open = false; int open_result = 100000; int last_open_flags = 0; bool override_write = false; std::atomic_int write_call_count = 0; int fail_write_call = -1; int short_write_call = -1; std::size_t short_write_size = 1; bool capture_input_events = false; std::vector written_input_events; bool override_ioctl = false; std::atomic_int ioctl_call_count = 0; int fail_ioctl_call = -1; bool override_poll = false; std::atomic_int poll_call_count = 0; std::vector poll_results; std::vector poll_revents; std::vector poll_errors; bool override_read = false; std::atomic_int read_call_count = 0; std::vector read_results; std::vector read_errors; uhid_event read_event {}; std::vector read_events; std::vector read_input_events; ff_effect uploaded_ff_effect {}; std::vector uploaded_ff_effects; std::atomic_size_t uploaded_ff_effect_index = 0; bool provide_uploaded_ff_effect = false; bool override_xtest_query = false; bool xtest_query_result = true; bool override_x_keycode = false; std::atomic_int x_keycode_call_count = 0; int fail_x_keycode_call = -1; bool override_libevdev = false; bool libevdev_new_returns_null = false; bool fail_libevdev_event_type = false; bool fail_libevdev_event_code = false; bool fail_libevdev_property = false; bool fail_libevdev_create = false; std::vector> owned_libevdev_devices; std::vector> owned_libevdev_uinput_devices; std::vector libevdev_devices; std::size_t libevdev_destroy_count = 0; }; LinuxTestSyscalls *&active_test_syscalls() { static LinuxTestSyscalls *syscalls = nullptr; return syscalls; } class ScopedLinuxTestSyscalls { public: explicit ScopedLinuxTestSyscalls(LinuxTestSyscalls &syscalls): previous_ {active_test_syscalls()} { active_test_syscalls() = &syscalls; } ScopedLinuxTestSyscalls(const ScopedLinuxTestSyscalls &) = delete; ScopedLinuxTestSyscalls &operator=(const ScopedLinuxTestSyscalls &) = delete; ScopedLinuxTestSyscalls(ScopedLinuxTestSyscalls &&) noexcept = delete; ScopedLinuxTestSyscalls &operator=(ScopedLinuxTestSyscalls &&) noexcept = delete; ~ScopedLinuxTestSyscalls() { active_test_syscalls() = previous_; } private: LinuxTestSyscalls *previous_ = nullptr; }; } // namespace } // namespace lvh::detail::test namespace { constexpr auto free_real_libevdev = &::libevdev_free; constexpr auto destroy_real_libevdev_uinput = &::libevdev_uinput_destroy; } // namespace int lvh_linux_test_access(const char *path, int mode) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_access) { if (lvh::detail::test::active_test_syscalls()->access_result < 0) { errno = EACCES; } return lvh::detail::test::active_test_syscalls()->access_result; } return ::access(path, mode); } int lvh_linux_test_open(const char *path, int flags) { if (lvh::detail::test::active_test_syscalls() != nullptr) { lvh::detail::test::active_test_syscalls()->last_open_flags = flags; } if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_open) { if (lvh::detail::test::active_test_syscalls()->open_result < 0) { errno = ENOENT; return lvh::detail::test::active_test_syscalls()->open_result; } const auto fd = ::open("/dev/null", O_RDWR); if (fd < 0) { errno = EIO; } return fd; } return ::open(path, flags); } std::ptrdiff_t lvh_linux_test_write(int fd, const std::byte *buffer, std::size_t size) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_write) { const auto call_count = ++lvh::detail::test::active_test_syscalls()->write_call_count; if (lvh::detail::test::active_test_syscalls()->fail_write_call == call_count) { errno = EIO; return -1; } if (lvh::detail::test::active_test_syscalls()->short_write_call == call_count) { return static_cast(lvh::detail::test::active_test_syscalls()->short_write_size); } if (lvh::detail::test::active_test_syscalls()->capture_input_events && size == sizeof(input_event)) { input_event event {}; std::memcpy(&event, buffer, sizeof(event)); lvh::detail::test::active_test_syscalls()->written_input_events.push_back({ .type = event.type, .code = event.code, .value = event.value, }); } return static_cast(size); } return static_cast(::write(fd, buffer, size)); } int lvh_linux_test_ioctl(int fd, unsigned long request, unsigned long argument) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_ioctl) { if (const auto call_count = ++lvh::detail::test::active_test_syscalls()->ioctl_call_count; lvh::detail::test::active_test_syscalls()->fail_ioctl_call == call_count) { errno = EINVAL; return -1; } if ( request == UI_BEGIN_FF_UPLOAD && lvh::detail::test::active_test_syscalls()->provide_uploaded_ff_effect ) { auto *upload = std::bit_cast(argument); auto &syscalls = *lvh::detail::test::active_test_syscalls(); const auto upload_index = syscalls.uploaded_ff_effect_index++; upload->effect = upload_index < syscalls.uploaded_ff_effects.size() ? syscalls.uploaded_ff_effects[upload_index] : syscalls.uploaded_ff_effect; } return 0; } return ::ioctl(fd, request, argument); } int lvh_linux_test_poll(pollfd *descriptors, nfds_t descriptor_count, int timeout) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_poll) { const auto call_index = static_cast(lvh::detail::test::active_test_syscalls()->poll_call_count++); auto result = 0; if (call_index < lvh::detail::test::active_test_syscalls()->poll_results.size()) { result = lvh::detail::test::active_test_syscalls()->poll_results[call_index]; } if (descriptor_count > 0) { descriptors[0].revents = 0; if (result > 0 && call_index < lvh::detail::test::active_test_syscalls()->poll_revents.size()) { descriptors[0].revents = lvh::detail::test::active_test_syscalls()->poll_revents[call_index]; } } if (result < 0) { errno = call_index < lvh::detail::test::active_test_syscalls()->poll_errors.size() ? lvh::detail::test::active_test_syscalls()->poll_errors[call_index] : EIO; } return result; } return ::poll(descriptors, descriptor_count, timeout); } std::ptrdiff_t lvh_linux_test_read(int fd, std::byte *buffer, std::size_t size) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_read) { const auto call_index = static_cast(lvh::detail::test::active_test_syscalls()->read_call_count++); auto result = std::ptrdiff_t {0}; if (call_index < lvh::detail::test::active_test_syscalls()->read_results.size()) { result = lvh::detail::test::active_test_syscalls()->read_results[call_index]; } if (result < 0) { errno = call_index < lvh::detail::test::active_test_syscalls()->read_errors.size() ? lvh::detail::test::active_test_syscalls()->read_errors[call_index] : EIO; return result; } if (result > 0) { if (const auto &input_events = lvh::detail::test::active_test_syscalls()->read_input_events; call_index < input_events.size()) { const auto bytes = std::min(static_cast(result), std::min(size, sizeof(input_event))); std::memcpy(buffer, &input_events[call_index], bytes); return static_cast(bytes); } const auto bytes = std::min(static_cast(result), std::min(size, sizeof(uhid_event))); const auto &read_events = lvh::detail::test::active_test_syscalls()->read_events; const auto &event = call_index < read_events.size() ? read_events[call_index] : lvh::detail::test::active_test_syscalls()->read_event; std::memcpy(buffer, &event, bytes); return static_cast(bytes); } return result; } return static_cast(::read(fd, buffer, size)); } #if defined(LIBVIRTUALHID_HAVE_XTEST) Display *lvh_linux_test_x_open_display(const char *) { return lvh::detail::test::fake_display_handle(); } int lvh_linux_test_x_close_display(Display *) { return 0; } Bool lvh_linux_test_xtest_query_extension(Display *, int *, int *, int *, int *) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_xtest_query) { return lvh::detail::test::active_test_syscalls()->xtest_query_result ? True : False; } return True; } KeyCode lvh_linux_test_x_keysym_to_keycode(Display *, KeySym keysym) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_x_keycode) { const auto call_count = ++lvh::detail::test::active_test_syscalls()->x_keycode_call_count; if (lvh::detail::test::active_test_syscalls()->fail_x_keycode_call == call_count) { return 0; } } return keysym == NoSymbol ? 0 : 1; } int lvh_linux_test_x_flush(Display *) { return 0; } int lvh_linux_test_default_screen(Display *) { return 0; } int lvh_linux_test_display_width(Display *, int) { return 1920; } int lvh_linux_test_display_height(Display *, int) { return 1080; } int lvh_linux_test_xtest_fake_key_event(Display *, unsigned int, Bool, unsigned long) { return 1; } int lvh_linux_test_xtest_fake_button_event(Display *, unsigned int, Bool, unsigned long) { return 1; } int lvh_linux_test_xtest_fake_motion_event(Display *, int, int, int, unsigned long) { return 1; } int lvh_linux_test_xtest_fake_relative_motion_event(Display *, int, int, unsigned long) { return 1; } #endif extern "C" libevdev *lvh_linux_test_libevdev_new() { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { if (lvh::detail::test::active_test_syscalls()->libevdev_new_returns_null) { return nullptr; } auto fake_device = std::make_unique(); auto *created_device = fake_device.get(); lvh::detail::test::active_test_syscalls()->owned_libevdev_devices.push_back(std::move(fake_device)); return lvh::detail::test::libevdev_handle(created_device); } return ::libevdev_new(); } extern "C" void lvh_linux_test_libevdev_free(libevdev *raw_libevdev_handle) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { auto *fake_device_handle = lvh::detail::test::fake_libevdev_device(raw_libevdev_handle); static_cast(std::erase_if( lvh::detail::test::active_test_syscalls()->owned_libevdev_devices, [fake_device_handle](const auto &owned_device) { return owned_device.get() == fake_device_handle; } )); return; } free_real_libevdev(raw_libevdev_handle); } extern "C" void lvh_linux_test_libevdev_set_name(libevdev *device, const char *name) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { lvh::detail::test::fake_libevdev_device(device)->name = name == nullptr ? "" : name; return; } ::libevdev_set_name(device, name); } extern "C" void lvh_linux_test_libevdev_set_id_bustype(libevdev *device, int bustype) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { lvh::detail::test::fake_libevdev_device(device)->bustype = static_cast(bustype); return; } ::libevdev_set_id_bustype(device, bustype); } extern "C" void lvh_linux_test_libevdev_set_id_vendor(libevdev *device, int vendor) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { lvh::detail::test::fake_libevdev_device(device)->vendor = static_cast(vendor); return; } ::libevdev_set_id_vendor(device, vendor); } extern "C" void lvh_linux_test_libevdev_set_id_product(libevdev *device, int product) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { lvh::detail::test::fake_libevdev_device(device)->product = static_cast(product); return; } ::libevdev_set_id_product(device, product); } extern "C" void lvh_linux_test_libevdev_set_id_version(libevdev *device, int version) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { lvh::detail::test::fake_libevdev_device(device)->version = static_cast(version); return; } ::libevdev_set_id_version(device, version); } extern "C" int lvh_linux_test_libevdev_enable_event_type(libevdev *device, unsigned int type) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { if (lvh::detail::test::active_test_syscalls()->fail_libevdev_event_type) { return -EIO; } lvh::detail::test::fake_libevdev_device(device)->event_types.push_back(type); return 0; } return ::libevdev_enable_event_type(device, type); } extern "C" int lvh_linux_test_libevdev_enable_event_code( libevdev *device, unsigned int type, unsigned int code, const void *data ) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { if (lvh::detail::test::active_test_syscalls()->fail_libevdev_event_code) { return -EIO; } lvh::detail::test::LinuxLibevdevEventCode event_code { .type = type, .code = code, }; if (type == EV_ABS && data != nullptr) { const auto *absinfo = static_cast(data); event_code.has_absinfo = true; event_code.minimum = absinfo->minimum; event_code.maximum = absinfo->maximum; event_code.fuzz = absinfo->fuzz; event_code.flat = absinfo->flat; event_code.resolution = absinfo->resolution; } lvh::detail::test::fake_libevdev_device(device)->event_codes.push_back(event_code); return 0; } return ::libevdev_enable_event_code(device, type, code, data); } extern "C" int lvh_linux_test_libevdev_enable_property(libevdev *device, unsigned int property) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { if (lvh::detail::test::active_test_syscalls()->fail_libevdev_property) { return -EIO; } lvh::detail::test::fake_libevdev_device(device)->properties.push_back(property); return 0; } return ::libevdev_enable_property(device, property); } extern "C" int lvh_linux_test_libevdev_uinput_create_from_device( const libevdev *device, int uinput_fd, libevdev_uinput **uinput_device ) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { if (lvh::detail::test::active_test_syscalls()->fail_libevdev_create || uinput_fd < 0) { return -EIO; } const auto &source_device = *lvh::detail::test::fake_libevdev_device(device); lvh::detail::test::active_test_syscalls()->libevdev_devices.push_back(source_device); auto owned_uinput_device = std::make_unique(lvh::detail::test::FakeLibevdevUinput {source_device}); auto *created_uinput_device = owned_uinput_device.get(); lvh::detail::test::active_test_syscalls()->owned_libevdev_uinput_devices.push_back(std::move(owned_uinput_device)); *uinput_device = lvh::detail::test::libevdev_uinput_handle(created_uinput_device); return 0; } return ::libevdev_uinput_create_from_device(device, uinput_fd, uinput_device); } extern "C" void lvh_linux_test_libevdev_uinput_destroy(libevdev_uinput *raw_uinput_handle) { if (lvh::detail::test::active_test_syscalls() != nullptr && lvh::detail::test::active_test_syscalls()->override_libevdev) { ++lvh::detail::test::active_test_syscalls()->libevdev_destroy_count; auto *fake_uinput_handle = lvh::detail::test::fake_libevdev_uinput(raw_uinput_handle); static_cast(std::erase_if( lvh::detail::test::active_test_syscalls()->owned_libevdev_uinput_devices, [fake_uinput_handle](const auto &owned_device) { return owned_device.get() == fake_uinput_handle; } )); return; } destroy_real_libevdev_uinput(raw_uinput_handle); } #define access lvh_linux_test_access #define ioctl lvh_linux_test_ioctl #define open lvh_linux_test_open #define poll lvh_linux_test_poll #define read lvh_linux_test_read #define write lvh_linux_test_write #define libevdev_enable_event_code lvh_linux_test_libevdev_enable_event_code #define libevdev_enable_event_type lvh_linux_test_libevdev_enable_event_type #define libevdev_enable_property lvh_linux_test_libevdev_enable_property #define libevdev_free lvh_linux_test_libevdev_free #define libevdev_new lvh_linux_test_libevdev_new #define libevdev_set_id_bustype lvh_linux_test_libevdev_set_id_bustype #define libevdev_set_id_product lvh_linux_test_libevdev_set_id_product #define libevdev_set_id_vendor lvh_linux_test_libevdev_set_id_vendor #define libevdev_set_id_version lvh_linux_test_libevdev_set_id_version #define libevdev_set_name lvh_linux_test_libevdev_set_name #define libevdev_uinput_create_from_device lvh_linux_test_libevdev_uinput_create_from_device #define libevdev_uinput_destroy lvh_linux_test_libevdev_uinput_destroy #if defined(LIBVIRTUALHID_HAVE_XTEST) #if defined(DefaultScreen) #undef DefaultScreen // NOSONAR(cpp:S959): X11 exposes this as a macro; the test hook replaces it while including the backend. #endif #if defined(DisplayHeight) #undef DisplayHeight // NOSONAR(cpp:S959): X11 exposes this as a macro; the test hook replaces it while including the backend. #endif #if defined(DisplayWidth) #undef DisplayWidth // NOSONAR(cpp:S959): X11 exposes this as a macro; the test hook replaces it while including the backend. #endif #define DefaultScreen lvh_linux_test_default_screen #define DisplayHeight lvh_linux_test_display_height #define DisplayWidth lvh_linux_test_display_width #define XCloseDisplay lvh_linux_test_x_close_display #define XFlush lvh_linux_test_x_flush #define XKeysymToKeycode lvh_linux_test_x_keysym_to_keycode #define XOpenDisplay lvh_linux_test_x_open_display #define XTestFakeButtonEvent lvh_linux_test_xtest_fake_button_event #define XTestFakeKeyEvent lvh_linux_test_xtest_fake_key_event #define XTestFakeMotionEvent lvh_linux_test_xtest_fake_motion_event #define XTestFakeRelativeMotionEvent lvh_linux_test_xtest_fake_relative_motion_event #define XTestQueryExtension lvh_linux_test_xtest_query_extension #endif #define create_platform_backend create_platform_backend_for_linux_backend_test_hooks #include "../../src/platform/linux/uhid_backend.cpp" #undef create_platform_backend #if defined(LIBVIRTUALHID_HAVE_XTEST) #undef XTestQueryExtension #undef XTestFakeRelativeMotionEvent #undef XTestFakeMotionEvent #undef XTestFakeKeyEvent #undef XTestFakeButtonEvent #undef XOpenDisplay #undef XKeysymToKeycode #undef XFlush #undef XCloseDisplay #undef DisplayWidth #undef DisplayHeight #undef DefaultScreen #endif #undef write #undef read #undef poll #undef open #undef ioctl #undef access #undef libevdev_uinput_destroy #undef libevdev_uinput_create_from_device #undef libevdev_set_name #undef libevdev_set_id_version #undef libevdev_set_id_vendor #undef libevdev_set_id_product #undef libevdev_set_id_bustype #undef libevdev_new #undef libevdev_free #undef libevdev_enable_property #undef libevdev_enable_event_type #undef libevdev_enable_event_code namespace lvh::detail::test { namespace { constexpr auto fake_fd = 100000; int open_test_fd() { return ::open("/dev/null", O_RDWR); } std::vector read_input_events_until_eof(int fd) { std::vector records; input_event event {}; while (::read(fd, &event, sizeof(event)) == sizeof(event)) { records.push_back({ .type = event.type, .code = event.code, .value = event.value, }); } return records; } bool write_uhid_event(int fd, const uhid_event &event) { const auto data = std::as_bytes(std::span {&event, 1U}); std::size_t written = 0; while (written < data.size()) { const auto result = ::write(fd, data.data() + written, data.size() - written); if (result <= 0) { return false; } written += static_cast(result); } return true; } bool read_uhid_event(int fd, uhid_event &event) { pollfd descriptor {}; descriptor.fd = fd; descriptor.events = POLLIN; if (const auto poll_result = ::poll(&descriptor, 1, 1000); poll_result <= 0 || (descriptor.revents & POLLIN) == 0) { return false; } const auto data = std::as_writable_bytes(std::span {&event, 1U}); std::size_t read_size = 0; while (read_size < data.size()) { const auto result = ::read(fd, data.data() + read_size, data.size() - read_size); if (result <= 0) { return false; } read_size += static_cast(result); } return true; } template bool read_uhid_event_matching(int fd, Predicate matches, uhid_event &event) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds {1}; while (std::chrono::steady_clock::now() < deadline) { if (!read_uhid_event(fd, event)) { return false; } if (matches(event)) { return true; } } return false; } bool read_uhid_event_type(int fd, unsigned int event_type, uhid_event &event) { return read_uhid_event_matching(fd, [event_type](const uhid_event &candidate) { return candidate.type == event_type; }, event); } std::vector> read_switch_input_reports(int fd, const std::vector &expected) { std::vector> reports; uhid_event event {}; for (auto index = 0; index < 2; ++index) { const auto received = read_uhid_event_matching(fd, [&expected](const uhid_event &candidate) { return candidate.type == UHID_INPUT2 && candidate.u.input2.size == expected.size() && candidate.u.input2.data[0] == expected[0] && std::equal(expected.begin() + 2, expected.end(), candidate.u.input2.data + 2U); }, event); if (!received) { break; } const auto *data = event.u.input2.data; reports.emplace_back(data, data + event.u.input2.size); } return reports; } OperationStatus create_started_uhid_gamepad( UhidGamepad &gamepad, DeviceId id, const CreateGamepadOptions &options, int peer_fd, uhid_event &create_event, bool &saw_create, bool &waited_for_start ) { auto create_status = OperationStatus::failure(ErrorCode::backend_failure, "UHID create thread did not run"); std::atomic_bool create_returned = false; std::jthread create_thread {[&]() { create_status = gamepad.create(id, options); create_returned = true; }}; saw_create = read_uhid_event_type(peer_fd, UHID_CREATE2, create_event); std::this_thread::sleep_for(std::chrono::milliseconds {20}); waited_for_start = !create_returned; uhid_event start_event {}; start_event.type = UHID_START; static_cast(write_uhid_event(peer_fd, start_event)); create_thread.join(); return create_status; } uhid_event create_started_profile_uhid_gamepad( UhidGamepad &gamepad, DeviceId id, const CreateGamepadOptions &options, int peer_fd, std::uint16_t expected_bus, LinuxUhidRoundTripResult &result ) { uhid_event event {}; result.create_status = create_started_uhid_gamepad( gamepad, id, options, peer_fd, event, result.creation.saw_create, result.creation.waited_for_start ); if (result.creation.saw_create) { const auto transport_profile = uhid_transport_profile(options.profile, options.metadata.has_battery); result.creation.saw_create = event.u.create2.vendor == uhid_vendor_id(transport_profile) && event.u.create2.product == transport_profile.product_id && event.u.create2.version == transport_profile.version && event.u.create2.bus == expected_bus; result.creation.bus = event.u.create2.bus; result.creation.vendor_id = event.u.create2.vendor; result.creation.product_id = event.u.create2.product; result.creation.version = event.u.create2.version; result.creation.name = reinterpret_cast(event.u.create2.name); result.creation.physical_id = reinterpret_cast(event.u.create2.phys); result.creation.unique_id = reinterpret_cast(event.u.create2.uniq); } return event; } std::uint32_t read_u32_le(const std::uint8_t *buffer) { return static_cast(buffer[0]) | (static_cast(buffer[1]) << 8U) | (static_cast(buffer[2]) << 16U) | (static_cast(buffer[3]) << 24U); } void enable_fake_device_syscalls(LinuxTestSyscalls &syscalls) { syscalls.override_access = true; syscalls.override_open = true; syscalls.override_write = true; syscalls.override_ioctl = true; syscalls.override_libevdev = true; } bool wait_for_poll_calls(const LinuxTestSyscalls &syscalls, int expected_calls) { using namespace std::chrono_literals; for (auto attempt = 0; attempt < 100; ++attempt) { if (syscalls.poll_call_count.load() >= expected_calls) { return true; } std::this_thread::sleep_for(1ms); } return false; } OperationStatus run_fake_uhid_read_loop(LinuxTestSyscalls &syscalls, int expected_poll_calls) { syscalls.override_write = true; syscalls.override_poll = true; syscalls.override_read = true; syscalls.poll_results.insert(syscalls.poll_results.begin(), 1); syscalls.poll_revents.insert(syscalls.poll_revents.begin(), POLLIN); syscalls.poll_errors.insert(syscalls.poll_errors.begin(), 0); syscalls.read_results.insert(syscalls.read_results.begin(), static_cast(sizeof(uhid_event))); syscalls.read_errors.insert(syscalls.read_errors.begin(), 0); uhid_event start_event {}; start_event.type = UHID_START; syscalls.read_events.insert(syscalls.read_events.begin(), start_event); ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; CreateGamepadOptions options; options.profile = profiles::generic_gamepad(); const auto fd = open_test_fd(); if (fd < 0) { return system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno); } UhidGamepad gamepad {fd}; if (const auto status = gamepad.create(1, options); !status.ok()) { return status; } const auto saw_expected_polls = wait_for_poll_calls(syscalls, expected_poll_calls + 1); const auto close_status = gamepad.close(); if (!saw_expected_polls) { return OperationStatus::failure(ErrorCode::backend_failure, "fake UHID read loop did not consume the scripted poll calls"); } return close_status; } DeviceProfile profile_for_uinput_device_type(DeviceType device_type) { switch (device_type) { case DeviceType::keyboard: return profiles::keyboard(); case DeviceType::mouse: return profiles::mouse(); case DeviceType::touchscreen: return profiles::touchscreen(); case DeviceType::trackpad: return profiles::trackpad(); case DeviceType::pen_tablet: return profiles::pen_tablet(); case DeviceType::gamepad: return profiles::xbox_series(); } return profiles::mouse(); } OperationStatus create_uinput_device_by_type( int fd, DeviceType device_type, std::optional gamepad_kind = std::nullopt, UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative ) { switch (device_type) { case DeviceType::keyboard: { CreateKeyboardOptions options; options.profile = profile_for_uinput_device_type(device_type); UinputKeyboard keyboard {fd}; return keyboard.create(1, options); } case DeviceType::mouse: { CreateMouseOptions options; options.profile = profile_for_uinput_device_type(device_type); UinputMouseDevice mouse {fd}; return mouse.create(1, options.profile, mouse_kind); } case DeviceType::touchscreen: { CreateTouchscreenOptions options; options.profile = profile_for_uinput_device_type(device_type); UinputTouchscreen touchscreen {fd}; return touchscreen.create(1, options); } case DeviceType::trackpad: { CreateTrackpadOptions options; options.profile = profile_for_uinput_device_type(device_type); UinputTrackpad trackpad {fd}; return trackpad.create(1, options); } case DeviceType::pen_tablet: { CreatePenTabletOptions options; options.profile = profile_for_uinput_device_type(device_type); UinputPenTablet pen_tablet {fd}; return pen_tablet.create(1, options); } case DeviceType::gamepad: { CreateGamepadOptions options; options.profile = gamepad_kind ? *profiles::gamepad_profile(*gamepad_kind) : profile_for_uinput_device_type(device_type); UinputGamepad gamepad {fd, options.profile.gamepad_kind}; return gamepad.create(1, options); } } return OperationStatus::failure(ErrorCode::unsupported_profile, "unsupported fake uinput device type"); } template LinuxLibevdevCreationResult create_fake_libevdev_device( DeviceType device_type, ConfigureFailure configure_failure, std::optional gamepad_kind = std::nullopt, UinputMouseDeviceKind mouse_kind = UinputMouseDeviceKind::relative ) { LinuxTestSyscalls syscalls; syscalls.override_libevdev = true; configure_failure(syscalls); ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxLibevdevCreationResult result; const auto fd = open_test_fd(); if (fd < 0) { result.status = system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno); return result; } result.status = create_uinput_device_by_type(fd, device_type, gamepad_kind, mouse_kind); if (!syscalls.libevdev_devices.empty()) { const auto &device = syscalls.libevdev_devices.back(); result.name = device.name; result.bustype = device.bustype; result.vendor = device.vendor; result.product = device.product; result.version = device.version; result.event_types = device.event_types; result.event_codes = device.event_codes; result.properties = device.properties; } result.destroy_count = syscalls.libevdev_destroy_count; if (result.status.ok()) { result.close_status = OperationStatus::success(); } return result; } void keep_fake_libevdev_successful(const LinuxTestSyscalls &syscalls) { static_cast(syscalls); } LinuxLibevdevCreationResult create_fake_libevdev_device(DeviceType device_type) { return create_fake_libevdev_device(device_type, keep_fake_libevdev_successful); } LinuxLibevdevCreationResult create_fake_libevdev_gamepad(GamepadProfileKind kind) { return create_fake_libevdev_device(DeviceType::gamepad, keep_fake_libevdev_successful, kind); } template LinuxInputSubmissionResult touch_contact_reuse_pipe() { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } TouchDevice device {descriptors[1]}; auto status = device.place_contact({.id = 0, .x = 0.1F, .y = 0.1F, .pressure = 0.5F}); if (status.ok()) { status = device.place_contact({.id = 1, .x = 0.2F, .y = 0.2F, .pressure = 0.5F}); } if (status.ok()) { status = device.release_contact(0, PointerTransition::release); } if (status.ok()) { status = device.place_contact({.id = 0, .x = 0.3F, .y = 0.3F, .pressure = 0.5F}); } if (status.ok()) { status = device.place_contact({.id = 1, .x = 0.25F, .y = 0.25F, .pressure = 0.5F}); } static_cast(device.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } } // namespace std::string linux_copy_string_char_buffer(const std::string &source) { std::array destination {}; copy_string(destination, source); return destination.data(); } int linux_key_code(KeyboardKeyCode key_code) { return key_code_to_linux(key_code); } int linux_mouse_button(MouseButton button) { return mouse_button_to_linux(button); } std::uint16_t linux_uhid_bus(BusType bus_type) { return to_uhid_bus(bus_type); } std::uint16_t linux_gamepad_uhid_bus(GamepadProfileKind kind) { const auto profile = profiles::gamepad_profile(kind); return profile ? to_uhid_bus(*profile) : to_uhid_bus(BusType::unknown); } std::uint16_t linux_uinput_bus(BusType bus_type) { return to_uinput_bus(bus_type); } int linux_absolute_axis(std::int32_t value, std::int32_t limit) { return scale_absolute_axis(value, limit); } int linux_absolute_axis_to_viewport( float value, std::int32_t source_dimension, std::int32_t viewport_offset, std::int32_t viewport_dimension, std::int32_t desktop_offset, std::int32_t desktop_dimension ) { return scale_absolute_axis_to_viewport( value, source_dimension, viewport_offset, viewport_dimension, desktop_offset, desktop_dimension ); } std::vector linux_decode_utf8(const std::string &text) { return decode_utf8(text); } std::string linux_uppercase_hex(std::uint32_t codepoint) { return uppercase_hex(codepoint); } KeyboardKeyCode linux_hex_digit_key_code(char digit) { return hex_digit_key_code(digit); } int linux_legacy_scroll_steps(std::int32_t distance) { return legacy_scroll_steps(distance); } int linux_pen_tool(PenToolType tool) { return pen_tool_to_linux(tool); } int linux_pen_button(PenButton button) { return pen_button_to_linux(button); } std::string linux_dualsense_mac_address(const std::string &stable_id, DeviceId id) { return format_mac_address(parse_mac_address(stable_id).value_or(generated_mac_address(id))); } bool linux_first_line_matches(std::string_view path, std::string_view expected) { const auto line = read_first_line(std::filesystem::path {std::string {path}}); return line && *line == expected; } bool linux_first_line_missing(std::string_view path) { return !read_first_line(std::filesystem::path {std::string {path}}); } bool linux_hidraw_name_matches(std::string_view path, std::string_view name) { return hidraw_name_matches(std::filesystem::path {std::string {path}}, name); } std::vector linux_discover_hidraw_nodes_by_metadata( std::string_view name, std::string_view physical_id, std::string_view unique_id, const std::string &hidraw_root ) { return discover_hidraw_nodes_by_metadata(name, physical_id, unique_id, hidraw_root); } std::vector linux_discover_nodes_by_name(const std::string &name) { return discover_input_nodes_by_name(name); } std::vector linux_discover_nodes_by_name( const std::string &name, const std::string &input_root, const std::string &hidraw_root ) { return discover_input_nodes_by_name(name, input_root, hidraw_root); } std::size_t linux_empty_device_nodes_count() { UhidGamepad gamepad {-1}; UinputKeyboard keyboard {-1}; UinputMouse mouse {-1, -1}; UinputTouchscreen touchscreen {-1}; UinputTrackpad trackpad {-1}; UinputPenTablet pen_tablet {-1}; return gamepad.device_nodes().size() + keyboard.device_nodes().size() + mouse.device_nodes().size() + touchscreen.device_nodes().size() + trackpad.device_nodes().size() + pen_tablet.device_nodes().size(); } std::size_t linux_uhid_descriptor_limit() { uhid_event event {}; return sizeof(event.u.create2.rd_data); } std::size_t linux_uhid_input_limit() { uhid_event event {}; return sizeof(event.u.input2.data); } OperationStatus linux_uhid_create_with_descriptor_size(std::size_t descriptor_size) { auto profile = profiles::generic_gamepad(); profile.report_descriptor.assign(descriptor_size, 0); CreateGamepadOptions options; options.profile = std::move(profile); UhidGamepad gamepad {-1}; return gamepad.create(1, options); } OperationStatus linux_uhid_submit_report_size(std::size_t report_size) { UhidGamepad gamepad {-1}; return gamepad.submit({}, std::vector(report_size, 0)); } OperationStatus linux_uhid_submit_after_close() { UhidGamepad gamepad {-1}; static_cast(gamepad.close()); return gamepad.submit({}, {0}); } OperationStatus linux_uinput_keyboard_create_invalid_fd() { CreateKeyboardOptions options; options.profile = profiles::keyboard(); UinputKeyboard keyboard {-1}; return keyboard.create(1, options); } OperationStatus linux_uinput_keyboard_submit_invalid_fd(const KeyboardEvent &event) { UinputKeyboard keyboard {-1}; return keyboard.submit(event); } OperationStatus linux_uinput_keyboard_type_text_invalid_fd(const std::string &text) { UinputKeyboard keyboard {-1}; return keyboard.type_text({.text = text}); } OperationStatus linux_uinput_keyboard_submit_after_close() { UinputKeyboard keyboard {-1}; static_cast(keyboard.close()); return keyboard.submit({.key_code = 0x41, .pressed = true}); } LinuxInputSubmissionResult linux_uinput_keyboard_submit_pipe(const KeyboardEvent &event) { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } UinputKeyboard keyboard {descriptors[1]}; auto status = keyboard.submit(event); static_cast(keyboard.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } LinuxInputSubmissionResult linux_uinput_gamepad_submit_pipe(GamepadProfileKind kind, const GamepadState &state) { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } const auto profile = profiles::gamepad_profile(kind); if (!profile) { static_cast(::close(descriptors[0])); static_cast(::close(descriptors[1])); return {OperationStatus::failure(ErrorCode::unsupported_profile, "unknown gamepad profile"), {}}; } UinputGamepad gamepad {descriptors[1], kind}; auto status = gamepad.submit(state, reports::pack_input_report(*profile, state)); static_cast(gamepad.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } LinuxUinputRumbleResult linux_uinput_gamepad_fake_rumble( GamepadProfileKind kind, std::uint16_t effect_type, std::uint16_t replay_length, bool send_stop, std::optional reupload_length ) { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.override_poll = true; syscalls.poll_results = {1, 1}; syscalls.poll_revents = {POLLIN, POLLIN}; syscalls.override_read = true; syscalls.read_results = { static_cast(sizeof(input_event)), static_cast(sizeof(input_event)), }; syscalls.read_input_events = { input_event {.type = EV_UINPUT, .code = UI_FF_UPLOAD, .value = 7}, input_event {.type = EV_FF, .code = 3, .value = 1}, }; if (reupload_length.has_value()) { syscalls.poll_results.push_back(1); syscalls.poll_revents.push_back(POLLIN); syscalls.read_results.push_back(static_cast(sizeof(input_event))); syscalls.read_input_events.push_back(input_event {.type = EV_UINPUT, .code = UI_FF_UPLOAD, .value = 8}); } if (send_stop) { syscalls.poll_results.push_back(1); syscalls.poll_revents.push_back(POLLIN); syscalls.read_results.push_back(static_cast(sizeof(input_event))); syscalls.read_input_events.push_back(input_event {.type = EV_FF, .code = 3, .value = 0}); } syscalls.poll_results.push_back(0); syscalls.poll_revents.push_back(0); syscalls.provide_uploaded_ff_effect = true; syscalls.uploaded_ff_effect.type = effect_type; syscalls.uploaded_ff_effect.id = 3; switch (effect_type) { case FF_RUMBLE: syscalls.uploaded_ff_effect.u.rumble.weak_magnitude = 0x1234; syscalls.uploaded_ff_effect.u.rumble.strong_magnitude = 0x5678; break; case FF_CONSTANT: syscalls.uploaded_ff_effect.u.constant.level = 0x1234; break; case FF_PERIODIC: syscalls.uploaded_ff_effect.u.periodic.waveform = FF_SINE; syscalls.uploaded_ff_effect.u.periodic.magnitude = 0x1234; break; case FF_RAMP: syscalls.uploaded_ff_effect.u.ramp.start_level = 0x1234; syscalls.uploaded_ff_effect.u.ramp.end_level = 0x2345; break; default: break; } syscalls.uploaded_ff_effect.replay.length = replay_length; if (reupload_length.has_value()) { auto replacement = syscalls.uploaded_ff_effect; replacement.replay.length = *reupload_length; syscalls.uploaded_ff_effects = {syscalls.uploaded_ff_effect, replacement}; } ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUinputRumbleResult result; const auto fd = open_test_fd(); if (fd < 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno); result.close_status = result.create_status; return result; } std::atomic_size_t callback_count = 0; std::atomic_size_t nonzero_callback_count = 0; std::atomic_size_t zero_callback_count = 0; std::mutex output_mutex; GamepadOutput last_output; UinputGamepad gamepad {fd, kind}; gamepad.set_output_callback([&callback_count, &nonzero_callback_count, &zero_callback_count, &last_output, &output_mutex]( const GamepadOutput &output ) { if (output.low_frequency_rumble != 0 || output.high_frequency_rumble != 0) { std::lock_guard lock {output_mutex}; last_output = output; ++nonzero_callback_count; } else { ++zero_callback_count; } ++callback_count; }); CreateGamepadOptions options; const auto profile = profiles::gamepad_profile(kind); if (!profile.has_value()) { result.create_status = OperationStatus::failure(ErrorCode::unsupported_profile, "unknown gamepad profile"); result.close_status = result.create_status; static_cast(system_close(fd)); return result; } options.profile = *profile; result.create_status = gamepad.create(8, options); const auto expect_zero_callback = send_stop || reupload_length.has_value(); const auto callbacks_complete = [&]() { return nonzero_callback_count.load() > 0 && (!expect_zero_callback || zero_callback_count.load() > 0); }; for (auto attempt = 0; attempt < 500 && !callbacks_complete(); ++attempt) { std::this_thread::sleep_for(std::chrono::milliseconds {1}); } result.nonzero_callback_count = nonzero_callback_count.load(); result.zero_callback_count_before_close = zero_callback_count.load(); result.close_status = gamepad.close(); result.callback_count = callback_count.load(); { std::lock_guard lock {output_mutex}; result.last_output = last_output; } return result; } LinuxRumbleCycleTiming linux_uinput_rumble_cycle_timing(std::uint64_t elapsed, std::uint64_t length) { const auto [cycle_elapsed, cycle_remaining] = rumble_effect_cycle_timing(elapsed, length); return {.elapsed = cycle_elapsed, .remaining = cycle_remaining}; } OperationStatus linux_uinput_user_device_invalid_fd() { return create_libevdev_uinput_device(-1, profiles::mouse(), 1).status; } OperationStatus linux_uinput_user_device_pipe() { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno); } auto status = create_libevdev_uinput_device(descriptors[1], profiles::mouse(), 1).status; static_cast(::close(descriptors[0])); static_cast(::close(descriptors[1])); return status; } OperationStatus linux_uinput_mouse_create_invalid_fd() { CreateMouseOptions options; options.profile = profiles::mouse(); UinputMouse mouse {-1, -1}; return mouse.create(1, options); } OperationStatus linux_uinput_mouse_submit_invalid_fd(const MouseEvent &event) { UinputMouse mouse {-1, -1}; return mouse.submit(event); } OperationStatus linux_uinput_mouse_submit_after_close() { UinputMouse mouse {-1, -1}; static_cast(mouse.close()); return mouse.submit({.kind = MouseEventKind::relative_motion, .x = 1, .y = 1}); } namespace { /** * @brief Submit mouse events through pipe-backed uinput devices. * * @param events Mouse events to submit. * @param options Mouse creation options containing optional viewport bounds. * @return Submission status and captured input events. */ LinuxInputSubmissionResult submit_uinput_mouse_pipe_sequence( const std::vector &events, const CreateMouseOptions &options ) { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } const auto absolute_descriptor = ::dup(descriptors[1]); if (absolute_descriptor < 0) { static_cast(::close(descriptors[0])); static_cast(::close(descriptors[1])); return {system_error_status(ErrorCode::backend_failure, "failed to duplicate pipe", errno), {}}; } UinputMouse mouse {descriptors[1], absolute_descriptor, options.desktop, options.viewport}; auto status = OperationStatus::success(); for (const auto &event : events) { status = mouse.submit(event); if (!status.ok()) { break; } } static_cast(mouse.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } } // namespace LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(const MouseEvent &event) { return submit_uinput_mouse_pipe_sequence(std::vector {event}, {}); } LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe( const MouseEvent &event, const CreateMouseOptions &options ) { return submit_uinput_mouse_pipe_sequence(std::vector {event}, options); } LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe_sequence(const std::vector &events) { return submit_uinput_mouse_pipe_sequence(events, {}); } LinuxMouseInputSubmissionResult linux_uinput_mouse_submit_split_pipe_sequence(const std::vector &events) { std::array relative_descriptors {-1, -1}; if (::pipe(relative_descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create relative mouse pipe", errno), {}, {}}; } std::array absolute_descriptors {-1, -1}; if (::pipe(absolute_descriptors.data()) != 0) { static_cast(::close(relative_descriptors[0])); static_cast(::close(relative_descriptors[1])); return {system_error_status(ErrorCode::backend_failure, "failed to create absolute mouse pipe", errno), {}, {}}; } UinputMouse mouse {relative_descriptors[1], absolute_descriptors[1]}; auto status = OperationStatus::success(); for (const auto &event : events) { status = mouse.submit(event); if (!status.ok()) { break; } } static_cast(mouse.close()); auto relative_records = read_input_events_until_eof(relative_descriptors[0]); auto absolute_records = read_input_events_until_eof(absolute_descriptors[0]); static_cast(::close(relative_descriptors[0])); static_cast(::close(absolute_descriptors[0])); return {std::move(status), std::move(relative_records), std::move(absolute_records)}; } LinuxInputSubmissionResult linux_uinput_touchscreen_contact_pipe(const TouchContact &contact) { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } UinputTouchscreen touchscreen {descriptors[1]}; auto status = touchscreen.place_contact(contact); if (status.ok()) { status = touchscreen.release_contact(contact.id, PointerTransition::release); } static_cast(touchscreen.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } LinuxInputSubmissionResult linux_uinput_trackpad_contact_pipe(const TouchContact &contact) { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } UinputTrackpad trackpad {descriptors[1]}; auto status = trackpad.place_contact(contact); if (status.ok()) { status = trackpad.button(true); } if (status.ok()) { status = trackpad.button(false); } if (status.ok()) { status = trackpad.release_contact(contact.id, PointerTransition::release); } static_cast(trackpad.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } LinuxInputSubmissionResult linux_uinput_pen_tablet_tool_pipe(const PenToolState &state) { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } UinputPenTablet pen_tablet {descriptors[1]}; auto status = pen_tablet.place_tool(state); if (status.ok()) { status = pen_tablet.button(PenButton::primary, true); } if (status.ok()) { status = pen_tablet.button(PenButton::primary, false); } static_cast(pen_tablet.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } LinuxInputSubmissionResult linux_uinput_trackpad_multi_contact_pipe() { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } UinputTrackpad trackpad {descriptors[1]}; auto status = OperationStatus::success(); for (auto id = 0; id < 5 && status.ok(); ++id) { status = trackpad.place_contact({ .id = id, .x = static_cast(id) / 5.0F, .y = 0.5F, .pressure = 0.5F, .orientation = id * 10, }); } for (auto id = 4; id >= 0 && status.ok(); --id) { status = trackpad.release_contact(id, PointerTransition::release); } static_cast(trackpad.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } LinuxInputSubmissionResult linux_uinput_touch_contact_reuse_pipe(DeviceType device_type) { switch (device_type) { case DeviceType::touchscreen: return touch_contact_reuse_pipe(); case DeviceType::trackpad: return touch_contact_reuse_pipe(); default: return { OperationStatus::failure(ErrorCode::invalid_argument, "device type must be touchscreen or trackpad"), {}, }; } } OperationStatus linux_uinput_touchscreen_invalid_contacts() { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno); } UinputTouchscreen touchscreen {descriptors[1]}; static_cast(touchscreen.place_contact({.id = -1})); auto status = OperationStatus::success(); for (auto id = 0; id <= touch_max_contacts && status.ok(); ++id) { status = touchscreen.place_contact({ .id = id, .x = 0.5F, .y = 0.5F, .pressure = 0.5F, .orientation = 0, }); } static_cast(touchscreen.close()); static_cast(read_input_events_until_eof(descriptors[0])); static_cast(::close(descriptors[0])); return status; } LinuxInputSubmissionResult linux_uinput_pen_tablet_transition_pipe() { std::array descriptors {-1, -1}; if (::pipe(descriptors.data()) != 0) { return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}}; } UinputPenTablet pen_tablet {descriptors[1]}; auto status = OperationStatus::success(); using enum PenToolType; constexpr std::array tools { pen, eraser, brush, pencil, airbrush, touch, unchanged, }; for (const auto tool : tools) { if (!status.ok()) { break; } status = pen_tablet.place_tool({ .tool = tool, .x = 0.25F, .y = 0.75F, .pressure = tool == eraser ? -1.0F : 0.25F, .distance = tool == eraser ? 0.5F : -1.0F, .tilt_x = 120.0F, .tilt_y = -120.0F, }); } if (status.ok()) { status = pen_tablet.button(PenButton::secondary, true); } if (status.ok()) { status = pen_tablet.button(PenButton::secondary, false); } if (status.ok()) { status = pen_tablet.button(PenButton::tertiary, true); } if (status.ok()) { status = pen_tablet.button(PenButton::tertiary, false); } static_cast(pen_tablet.close()); auto records = read_input_events_until_eof(descriptors[0]); static_cast(::close(descriptors[0])); return {std::move(status), std::move(records)}; } OperationStatus linux_uinput_pen_tablet_closed_status() { UinputPenTablet pen_tablet {-1}; static_cast(pen_tablet.close()); static_cast(pen_tablet.place_tool({})); return pen_tablet.button(PenButton::primary, true); } LinuxUhidRoundTripResult linux_uhid_socketpair_roundtrip() { LinuxUhidRoundTripResult result; std::array descriptors {-1, -1}; if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); result.submit_status = result.create_status; result.close_status = result.create_status; return result; } auto profile = profiles::xbox_360(); CreateGamepadOptions options; options.profile = profile; options.metadata.stable_id = "linux-uhid-roundtrip"; UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 7, options, descriptors[1], BUS_USB, result); gamepad.set_output_callback([&result](const GamepadOutput &output) { ++result.output.callback_count; result.output.last = output; }); event = {}; event.type = UHID_OUTPUT; event.u.output.size = static_cast<__u16>(profile.output_report_size); event.u.output.data[0] = profile.report_id; event.u.output.data[1] = 0x34; event.u.output.data[2] = 0x12; event.u.output.data[3] = 0x78; event.u.output.data[4] = 0x56; static_cast(write_uhid_event(descriptors[1], event)); event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 9; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event(descriptors[1], event)) { result.saw_get_report_reply = event.type == UHID_GET_REPORT_REPLY && event.u.get_report_reply.id == 9; } event = {}; event.type = UHID_SET_REPORT; event.u.set_report.id = 10; event.u.set_report.rnum = profile.report_id; event.u.set_report.rtype = UHID_OUTPUT_REPORT; event.u.set_report.size = static_cast<__u16>(profile.output_report_size); event.u.set_report.data[0] = profile.report_id; event.u.set_report.data[1] = 0x78; event.u.set_report.data[2] = 0x56; event.u.set_report.data[3] = 0x34; event.u.set_report.data[4] = 0x12; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event(descriptors[1], event)) { result.saw_set_report_reply = event.type == UHID_SET_REPORT_REPLY && event.u.set_report_reply.id == 10; } event = {}; event.type = UHID_OPEN; static_cast(write_uhid_event(descriptors[1], event)); lvh::GamepadState state; state.buttons.set(GamepadButton::a); const auto report = reports::pack_input_report(profile, state); result.submit_status = gamepad.submit(state, report); if (read_uhid_event(descriptors[1], event)) { result.saw_input = event.type == UHID_INPUT2 && event.u.input2.size == report.size(); } result.close_status = gamepad.close(); if (read_uhid_event(descriptors[1], event)) { result.saw_destroy = event.type == UHID_DESTROY; } static_cast(::close(descriptors[1])); return result; } LinuxUhidRoundTripResult linux_xbox_bluetooth_uhid_socketpair_reports( GamepadProfileKind kind, bool advertises_battery, bool submits_battery ) { LinuxUhidRoundTripResult result; std::array descriptors {-1, -1}; if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); result.submit_status = result.create_status; result.close_status = result.create_status; return result; } const auto profile = profiles::gamepad_profile(kind).value(); CreateGamepadOptions options; options.profile = profile; options.metadata.stable_id = "xbox-bluetooth-uhid-roundtrip"; options.metadata.has_battery = advertises_battery; UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 13, options, descriptors[1], BUS_BLUETOOTH, result); if (result.creation.saw_create) { const auto series = kind == GamepadProfileKind::xbox_series; const auto descriptor = std::span { event.u.create2.rd_data, event.u.create2.rd_size, }; constexpr std::array battery_report_descriptor { 0x05U, 0x06U, 0x09U, 0x20U, 0x85U, xbox_bluetooth_battery_report_id, 0x15U, 0x04U, 0x25U, 0x07U, 0x75U, 0x08U, 0x95U, 0x01U, 0x81U, 0x02U, }; result.xbox.saw_transport_descriptor = descriptor.size() == (advertises_battery ? 299U : 283U); result.xbox.saw_battery_descriptor = std::ranges::search(descriptor, battery_report_descriptor).begin() != descriptor.end(); constexpr std::array right_stick_usages { 0x09U, 0x33U, 0x09U, 0x34U, }; constexpr std::array left_trigger_usage { 0x05U, 0x01U, 0x09U, 0x32U, 0x15U, 0x00U, }; constexpr std::array right_trigger_usage { 0x05U, 0x01U, 0x09U, 0x35U, 0x15U, 0x00U, }; result.xbox.saw_canonical_evdev_axes = std::ranges::search(descriptor, right_stick_usages).begin() != descriptor.end() && std::ranges::search(descriptor, left_trigger_usage).begin() != descriptor.end() && std::ranges::search(descriptor, right_trigger_usage).begin() != descriptor.end(); result.xbox.saw_gamepad_application_usage = event.u.create2.rd_size >= 4U && event.u.create2.rd_data[0] == 0x05U && event.u.create2.rd_data[1] == 0x01U && event.u.create2.rd_data[2] == 0x09U && event.u.create2.rd_data[3] == 0x05U; result.xbox.saw_bluetooth_identity = static_cast(event.u.create2.vendor) == 0x045EU && event.u.create2.product == (series ? xbox_series_uinput_product_id : xbox_wireless_uinput_product_id) && event.u.create2.version == xbox_bluetooth_version && event.u.create2.bus == BUS_BLUETOOTH; } std::atomic_size_t callback_count {0}; gamepad.set_output_callback([&result, &callback_count](const GamepadOutput &output) { result.output.last = output; if (output.kind == GamepadOutputKind::rumble) { result.output.rumble = output; } else if (output.kind == GamepadOutputKind::trigger_rumble) { result.output.trigger_rumble = output; } ++callback_count; }); event = {}; event.type = UHID_OPEN; static_cast(write_uhid_event(descriptors[1], event)); GamepadState state; state.buttons.set(GamepadButton::a); state.buttons.set(GamepadButton::start); state.buttons.set(GamepadButton::dpad_right); state.buttons.set(GamepadButton::left_shoulder); state.buttons.set(GamepadButton::left_stick); state.buttons.set(GamepadButton::guide); if (kind == GamepadProfileKind::xbox_series) { state.buttons.set(GamepadButton::misc1); } state.left_stick = {.x = 1.0F, .y = -1.0F}; state.right_stick = {.x = 0.5F, .y = -0.5F}; state.left_trigger = 0.25F; state.right_trigger = 0.75F; if (submits_battery) { state.battery = GamepadBattery { .state = GamepadBatteryState::discharging, .percentage = 50, }; } const auto report = reports::pack_input_report(profile, state); result.submit_status = gamepad.submit(state, report); if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto input = std::span {event.u.input2.data, event.u.input2.size}; result.xbox.saw_input = input.size() == xbox_bluetooth_input_report_size && input[0] == xbox_bluetooth_input_report_id && std::equal(report.begin(), report.begin() + 8, input.begin() + 1) && std::equal(report.begin() + 8, report.begin() + 12, input.begin() + 9) && input[13] == report[14] && input[14] == 0x41U && input[15] == 0x38U; result.xbox.saw_guide = input.size() == xbox_bluetooth_input_report_size && (input[15] & 0x10U) != 0U; result.xbox.saw_profile_consumer_button = input.size() == xbox_bluetooth_input_report_size && input[16] == (kind == GamepadProfileKind::xbox_series ? 0x01U : 0x00U); } result.saw_input = result.xbox.saw_input; if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto input = std::span {event.u.input2.data, event.u.input2.size}; result.xbox.saw_battery_input = input.size() == xbox_bluetooth_battery_report_size && input[0] == xbox_bluetooth_battery_report_id && input[1] == 0x05U; } constexpr std::array motor_report { xbox_bluetooth::xbox_bluetooth_rumble_report_id, 0x0F, 25, 50, 75, 100, 10, 0, 0, }; event = {}; event.type = UHID_OUTPUT; event.u.output.rtype = UHID_OUTPUT_REPORT; event.u.output.size = static_cast<__u16>(motor_report.size()); std::copy(motor_report.begin(), motor_report.end(), event.u.output.data); static_cast(write_uhid_event(descriptors[1], event)); const auto output_deadline = std::chrono::steady_clock::now() + std::chrono::seconds {1}; while (std::chrono::steady_clock::now() < output_deadline && callback_count.load() < 2U) { std::this_thread::sleep_for(std::chrono::milliseconds {10}); } result.output.callback_count = callback_count.load(); result.close_status = gamepad.close(); if (read_uhid_event_type(descriptors[1], UHID_DESTROY, event)) { result.saw_destroy = true; } static_cast(::close(descriptors[1])); return result; } bool linux_gamepad_prefers_uhid(GamepadProfileKind kind) { return prefers_uhid_gamepad_profile(kind); } bool linux_gamepad_uses_uinput(GamepadProfileKind kind) { return uses_uinput_gamepad_profile(kind); } DeviceProfile linux_uinput_effective_gamepad_profile(GamepadProfileKind kind) { const auto requested_profile = profiles::gamepad_profile(kind).value(); return effective_uinput_profile(requested_profile).value_or(requested_profile); } LinuxUhidRoundTripResult linux_switch_pro_uhid_socketpair_reports() { LinuxUhidRoundTripResult result; std::array descriptors {-1, -1}; if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); result.submit_status = result.create_status; result.close_status = result.create_status; return result; } CreateGamepadOptions options; options.profile = profiles::switch_pro(); options.metadata.stable_id = "streaming-host-gamepad-0"; UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 12, options, descriptors[1], BUS_BLUETOOTH, result); result.creation.saw_create = result.creation.saw_create && event.u.create2.rd_size == options.profile.report_descriptor.size(); const auto idle_report = reports::pack_input_report(options.profile, {}); result.switch_pro.idle_input_reports = read_switch_input_reports(descriptors[1], idle_report); gamepad.set_output_callback([&result](const GamepadOutput &output) { ++result.output.callback_count; result.output.last = output; if ( output.kind == GamepadOutputKind::player_leds && output.player_leds == std::array {true, false, true, false} && output.flashing_player_leds == std::array {false, true, false, true} ) { result.switch_pro.saw_player_leds = true; } }); event = {}; event.type = UHID_OUTPUT; event.u.output.rtype = UHID_OUTPUT_REPORT; event.u.output.size = static_cast<__u16>(options.profile.output_report_size); event.u.output.data[0] = 0x01; event.u.output.data[1] = 0x07; const std::array neutral_rumble {0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40}; std::ranges::copy(neutral_rumble, event.u.output.data + 2U); event.u.output.data[10] = 0x30; event.u.output.data[11] = 0xA5; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_matching(descriptors[1], [](const uhid_event &candidate) { return candidate.type == UHID_INPUT2 && candidate.u.input2.size > 0U && candidate.u.input2.data[0] == 0x21; }, event)) { if (event.u.input2.size > 1U) { result.switch_pro.subcommand_reply_packet_timer = static_cast(event.u.input2.data[1]); } result.switch_pro.saw_subcommand_reply = event.u.input2.size == options.profile.input_report_size && event.u.input2.data[0] == 0x21 && event.u.input2.data[13] == 0x80 && event.u.input2.data[14] == 0x30; } GamepadState state; state.buttons.set(GamepadButton::a); state.acceleration = Vector3 {.x = 9.80665F, .y = 19.6133F, .z = -9.80665F}; state.gyroscope = Vector3 {.x = 1.0F, .y = 2.0F, .z = -3.0F}; const auto report = reports::pack_input_report(options.profile, state); result.submit_status = gamepad.submit(state, report); result.switch_pro.repeated_input_reports = read_switch_input_reports(descriptors[1], report); if (!result.switch_pro.repeated_input_reports.empty()) { const auto &motion_report = result.switch_pro.repeated_input_reports.front(); result.switch_pro.motion_input_packet_timer = motion_report[1]; result.switch_pro.saw_motion_input = motion_report[0] == 0x30 && (motion_report[3] & 0x08U) != 0U && std::equal(report.begin() + 13, report.begin() + 49, motion_report.begin() + 13); } result.switch_pro.clear_status = gamepad.submit({}, reports::pack_input_report(options.profile, {})); result.switch_pro.cleared_input_reports = read_switch_input_reports(descriptors[1], idle_report); result.close_status = gamepad.close(); result.saw_destroy = read_uhid_event_type(descriptors[1], UHID_DESTROY, event); result.switch_pro.stopped_after_destroy = result.saw_destroy && !read_uhid_event(descriptors[1], event); static_cast(::close(descriptors[1])); return result; } LinuxUhidRoundTripResult linux_dualsense_uhid_socketpair_reports() { LinuxUhidRoundTripResult result; std::array descriptors {-1, -1}; if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); result.submit_status = result.create_status; result.close_status = result.create_status; return result; } CreateGamepadOptions options; options.profile = profiles::dualsense_usb(); options.profile.name = "libvirtualhid PS5 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 8, options, descriptors[1], BUS_USB, result); gamepad.set_output_callback([&result](const GamepadOutput &output) { if (output.kind == GamepadOutputKind::rumble) { ++result.output.callback_count; result.output.last = output; } }); event = {}; event.type = UHID_OUTPUT; event.u.output.rtype = UHID_OUTPUT_REPORT; event.u.output.size = 63; event.u.output.data[0] = 0x02; event.u.output.data[1] = 0x03; event.u.output.data[3] = 0x12; event.u.output.data[4] = 0x56; static_cast(write_uhid_event(descriptors[1], event)); event = {}; event.type = UHID_SET_REPORT; event.u.set_report.id = 20; event.u.set_report.rnum = 0x02; event.u.set_report.rtype = UHID_OUTPUT_REPORT; event.u.set_report.size = 62; event.u.set_report.data[0] = 0x03; event.u.set_report.data[2] = 0x12; event.u.set_report.data[3] = 0x56; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_SET_REPORT_REPLY, event)) { result.saw_set_report_reply = event.u.set_report_reply.id == 20 && event.u.set_report_reply.err == 0; } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 11; event.u.get_report.rnum = 0x05; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { result.dualsense.saw_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 41U && event.u.get_report_reply.data[0] == 0x05; } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 12; event.u.get_report.rnum = 0x09; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { result.dualsense.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 7 && event.u.get_report_reply.data[0] == 0x09 && event.u.get_report_reply.data[1] == 0x07 && event.u.get_report_reply.data[6] == 0x02; } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 13; event.u.get_report.rnum = 0x20; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { result.dualsense.saw_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size > 0 && event.u.get_report_reply.data[0] == 0x20; } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 15; event.u.get_report.rnum = 0x7F; static_cast(write_uhid_event(descriptors[1], event)); static_cast(read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)); result.close_status = gamepad.close(); static_cast(::close(descriptors[1])); result.submit_status = OperationStatus::success(); return result; } LinuxUhidRoundTripResult linux_dualsense_bluetooth_uhid_socketpair_reports() { LinuxUhidRoundTripResult result; std::array descriptors {-1, -1}; if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); result.submit_status = result.create_status; result.close_status = result.create_status; return result; } CreateGamepadOptions options; options.profile = profiles::dualsense_bluetooth(); options.profile.name = "libvirtualhid PS5 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 9, options, descriptors[1], BUS_BLUETOOTH, result); GamepadState motion_state; motion_state.acceleration = Vector3 {.x = 1.0F, .y = 2.0F, .z = 3.0F}; motion_state.gyroscope = Vector3 {.x = 4.0F, .y = 5.0F, .z = 6.0F}; const auto submitted_report = reports::pack_input_report(options.profile, motion_state); result.submit_status = gamepad.submit(motion_state, submitted_report); bool first_input_report_valid = false; std::uint8_t first_sequence = 0; std::uint32_t first_sensor_timestamp = 0; std::array first_sensor_values {}; if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto report_size = static_cast(event.u.input2.size); if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); std::copy_n(event.u.input2.data + 17U, first_sensor_values.size(), first_sensor_values.begin()); first_input_report_valid = expected_crc == actual_crc && std::equal( first_sensor_values.begin(), first_sensor_values.end(), submitted_report.begin() + 17 ); first_sequence = event.u.input2.data[8]; first_sensor_timestamp = read_u32_le(event.u.input2.data + 29U); } } if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto report_size = static_cast(event.u.input2.size); if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); result.dualsense.saw_bluetooth_input_with_live_sensor_metadata = first_input_report_valid && expected_crc == actual_crc && event.u.input2.data[8] != first_sequence && read_u32_le(event.u.input2.data + 29U) != first_sensor_timestamp && std::equal(first_sensor_values.begin(), first_sensor_values.end(), event.u.input2.data + 17U); } } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 14; event.u.get_report.rnum = 0x09; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { const auto report_size = static_cast(event.u.get_report_reply.size); result.dualsense.saw_pairing = event.u.get_report_reply.err == 0 && report_size > 7U && event.u.get_report_reply.data[0] == 0x09 && event.u.get_report_reply.data[1] == 0x07 && event.u.get_report_reply.data[6] == 0x02; if (report_size >= 4U) { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32( std::span {event.u.get_report_reply.data, crc_offset}, playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed) ); const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset); result.dualsense.saw_feature_crc = expected_crc == actual_crc; } } result.close_status = gamepad.close(); static_cast(::close(descriptors[1])); return result; } LinuxUhidRoundTripResult linux_dualshock4_uhid_socketpair_reports() { LinuxUhidRoundTripResult result; std::array descriptors {-1, -1}; if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); result.submit_status = result.create_status; result.close_status = result.create_status; return result; } CreateGamepadOptions options; options.profile = profiles::dualshock4_usb(); options.profile.name = "libvirtualhid PS4 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 10, options, descriptors[1], BUS_USB, result); result.creation.saw_create = result.creation.saw_create && event.u.create2.rd_size == options.profile.report_descriptor.size(); if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { result.dualshock4.saw_usb_input = event.u.input2.size == options.profile.input_report_size && event.u.input2.data[0] == options.profile.report_id; } gamepad.set_output_callback([&result](const GamepadOutput &output) { if (output.kind == GamepadOutputKind::rumble) { ++result.output.callback_count; result.output.last = output; } }); event = {}; event.type = UHID_SET_REPORT; event.u.set_report.id = 21; event.u.set_report.rnum = 0x05; event.u.set_report.rtype = UHID_OUTPUT_REPORT; event.u.set_report.size = 31; event.u.set_report.data[0] = 0x03; event.u.set_report.data[3] = 0x12; event.u.set_report.data[4] = 0x56; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_SET_REPORT_REPLY, event)) { result.saw_set_report_reply = event.u.set_report_reply.id == 21 && event.u.set_report_reply.err == 0; } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 16; event.u.get_report.rnum = 0x02; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { result.dualshock4.saw_calibration = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 37 && event.u.get_report_reply.data[0] == 0x02; } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 17; event.u.get_report.rnum = 0x12; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { result.dualshock4.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 && event.u.get_report_reply.data[0] == 0x12 && event.u.get_report_reply.data[1] == 0x07 && event.u.get_report_reply.data[6] == 0x02; } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 18; event.u.get_report.rnum = 0xA3; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { result.dualshock4.saw_firmware = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 49 && event.u.get_report_reply.data[0] == 0xA3; } result.close_status = gamepad.close(); static_cast(::close(descriptors[1])); result.submit_status = OperationStatus::success(); return result; } LinuxUhidRoundTripResult linux_dualshock4_bluetooth_uhid_socketpair_reports() { LinuxUhidRoundTripResult result; std::array descriptors {-1, -1}; if (::socketpair(AF_UNIX, SOCK_STREAM, 0, descriptors.data()) != 0) { result.create_status = system_error_status(ErrorCode::backend_failure, "failed to create socketpair", errno); result.submit_status = result.create_status; result.close_status = result.create_status; return result; } CreateGamepadOptions options; options.profile = profiles::dualshock4_bluetooth(); options.profile.name = "libvirtualhid PS4 Controller"; options.metadata.stable_id = "02:03:04:05:06:07"; UhidGamepad gamepad {descriptors[0]}; auto event = create_started_profile_uhid_gamepad(gamepad, 11, options, descriptors[1], BUS_BLUETOOTH, result); if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) { const auto report_size = static_cast(event.u.input2.size); if ( report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x11 && (event.u.input2.data[1] & 0x80U) != 0U ) { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32(std::span {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1)); const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset); result.dualshock4.saw_bluetooth_input = expected_crc == actual_crc; } } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 19; event.u.get_report.rnum = 0x05; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { const auto report_size = static_cast(event.u.get_report_reply.size); result.dualshock4.saw_calibration = event.u.get_report_reply.err == 0 && report_size == 41U && event.u.get_report_reply.data[0] == 0x05; if (report_size >= 4U) { const auto crc_offset = report_size - 4U; const auto expected_crc = crc32( std::span {event.u.get_report_reply.data, crc_offset}, playstation_crc_seed(playstation_feature_reports::playstation_feature_crc_seed) ); const auto actual_crc = read_u32_le(event.u.get_report_reply.data + crc_offset); result.dualshock4.saw_feature_crc = expected_crc == actual_crc; } } event = {}; event.type = UHID_GET_REPORT; event.u.get_report.id = 20; event.u.get_report.rnum = 0x12; static_cast(write_uhid_event(descriptors[1], event)); if (read_uhid_event_type(descriptors[1], UHID_GET_REPORT_REPLY, event)) { result.dualshock4.saw_pairing = event.u.get_report_reply.err == 0 && event.u.get_report_reply.size == 16 && event.u.get_report_reply.data[0] == 0x12 && event.u.get_report_reply.data[1] == 0x07 && event.u.get_report_reply.data[6] == 0x02; } result.close_status = gamepad.close(); static_cast(::close(descriptors[1])); result.submit_status = OperationStatus::success(); return result; } LinuxBackendFakeCreationResult linux_backend_create_all_fake_success() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxBackendFakeCreationResult result; LinuxUhidBackend backend; result.capabilities = backend.capabilities(); CreateGamepadOptions gamepad_options; gamepad_options.profile = profiles::xbox_360(); gamepad_options.metadata.stable_id = "fake-linux-gamepad"; auto gamepad = backend.create_gamepad(1, gamepad_options); result.gamepad_status = gamepad.status; if (gamepad) { result.gamepad_close_status = gamepad.gamepad->close(); } CreateKeyboardOptions keyboard_options; keyboard_options.profile = profiles::keyboard(); keyboard_options.stable_id = "fake-linux-keyboard"; auto keyboard = backend.create_keyboard(2, keyboard_options); result.keyboard_status = keyboard.status; if (keyboard) { result.keyboard_close_status = keyboard.keyboard->close(); } CreateMouseOptions mouse_options; mouse_options.profile = profiles::mouse(); mouse_options.stable_id = "fake-linux-mouse"; auto mouse = backend.create_mouse(3, mouse_options); result.mouse_status = mouse.status; if (mouse) { result.mouse_close_status = mouse.mouse->close(); } CreateTouchscreenOptions touchscreen_options; touchscreen_options.profile = profiles::touchscreen(); touchscreen_options.stable_id = "fake-linux-touchscreen"; auto touchscreen = backend.create_touchscreen(4, touchscreen_options); result.touchscreen_status = touchscreen.status; if (touchscreen) { result.touchscreen_close_status = touchscreen.touchscreen->close(); } CreateTrackpadOptions trackpad_options; trackpad_options.profile = profiles::trackpad(); trackpad_options.stable_id = "fake-linux-trackpad"; auto trackpad = backend.create_trackpad(5, trackpad_options); result.trackpad_status = trackpad.status; if (trackpad) { result.trackpad_close_status = trackpad.trackpad->close(); } CreatePenTabletOptions pen_tablet_options; pen_tablet_options.profile = profiles::pen_tablet(); pen_tablet_options.stable_id = "fake-linux-pen-tablet"; auto pen_tablet = backend.create_pen_tablet(6, pen_tablet_options); result.pen_tablet_status = pen_tablet.status; if (pen_tablet) { result.pen_tablet_close_status = pen_tablet.pen_tablet->close(); } return result; } BackendCapabilities linux_backend_fake_unavailable_capabilities() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.access_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; return backend.capabilities(); } OperationStatus linux_backend_gamepad_fake_open_failure() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.override_open = true; syscalls.open_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateGamepadOptions options; options.profile = profiles::xbox_360(); return backend.create_gamepad(1, options).status; } int linux_backend_gamepad_open_flags() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.override_open = true; syscalls.open_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateGamepadOptions options; options.profile = profiles::dualshock4_usb(); static_cast(backend.create_gamepad(1, options)); return syscalls.last_open_flags; } OperationStatus linux_backend_gamepad_fake_create_failure() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_write_call = 1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateGamepadOptions options; options.profile = profiles::dualshock4_usb(); return backend.create_gamepad(1, options).status; } OperationStatus linux_backend_keyboard_fake_open_failure() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.override_open = true; syscalls.open_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateKeyboardOptions options; options.profile = profiles::keyboard(); return backend.create_keyboard(1, options).status; } OperationStatus linux_backend_keyboard_fake_create_failure() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateKeyboardOptions options; options.profile = profiles::keyboard(); return backend.create_keyboard(1, options).status; } OperationStatus linux_backend_keyboard_fake_fallback_success() { #if defined(LIBVIRTUALHID_HAVE_XTEST) LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateKeyboardOptions options; options.profile = profiles::keyboard(); auto keyboard = backend.create_keyboard(1, options); if (!keyboard) { return keyboard.status; } return keyboard.keyboard->close(); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_backend_mouse_fake_open_failure() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.override_open = true; syscalls.open_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateMouseOptions options; options.profile = profiles::mouse(); return backend.create_mouse(1, options).status; } OperationStatus linux_backend_mouse_fake_create_failure() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateMouseOptions options; options.profile = profiles::mouse(); return backend.create_mouse(1, options).status; } OperationStatus linux_backend_mouse_fake_fallback_success() { #if defined(LIBVIRTUALHID_HAVE_XTEST) LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateMouseOptions options; options.profile = profiles::mouse(); auto mouse = backend.create_mouse(1, options); if (!mouse) { return mouse.status; } return mouse.mouse->close(); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_backend_keyboard_fake_create_failure_without_fallback() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; syscalls.override_xtest_query = true; syscalls.xtest_query_result = false; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateKeyboardOptions options; options.profile = profiles::keyboard(); return backend.create_keyboard(1, options).status; } OperationStatus linux_backend_mouse_fake_create_failure_without_fallback() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; syscalls.override_xtest_query = true; syscalls.xtest_query_result = false; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateMouseOptions options; options.profile = profiles::mouse(); return backend.create_mouse(1, options).status; } OperationStatus linux_backend_touchscreen_fake_open_failure() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.override_open = true; syscalls.open_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateTouchscreenOptions options; options.profile = profiles::touchscreen(); return backend.create_touchscreen(1, options).status; } OperationStatus linux_backend_touchscreen_fake_create_failure() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateTouchscreenOptions options; options.profile = profiles::touchscreen(); return backend.create_touchscreen(1, options).status; } OperationStatus linux_backend_trackpad_fake_open_failure() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.override_open = true; syscalls.open_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateTrackpadOptions options; options.profile = profiles::trackpad(); return backend.create_trackpad(1, options).status; } OperationStatus linux_backend_trackpad_fake_create_failure() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreateTrackpadOptions options; options.profile = profiles::trackpad(); return backend.create_trackpad(1, options).status; } OperationStatus linux_backend_pen_tablet_fake_open_failure() { LinuxTestSyscalls syscalls; syscalls.override_access = true; syscalls.override_open = true; syscalls.open_result = -1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreatePenTabletOptions options; options.profile = profiles::pen_tablet(); return backend.create_pen_tablet(1, options).status; } OperationStatus linux_backend_pen_tablet_fake_create_failure() { LinuxTestSyscalls syscalls; enable_fake_device_syscalls(syscalls); syscalls.fail_libevdev_create = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; LinuxUhidBackend backend; CreatePenTabletOptions options; options.profile = profiles::pen_tablet(); return backend.create_pen_tablet(1, options).status; } OperationStatus linux_uhid_submit_fake_write_failure() { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.fail_write_call = 1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UhidGamepad gamepad {fake_fd}; return gamepad.submit({}, {0}); } OperationStatus linux_uhid_submit_fake_short_write() { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.short_write_call = 1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UhidGamepad gamepad {fake_fd}; return gamepad.submit({}, {0}); } OperationStatus linux_uhid_close_fake_write_failure() { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.fail_write_call = 1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UhidGamepad gamepad {fake_fd}; return gamepad.close(); } OperationStatus linux_uhid_close_fake_close_failure() { LinuxTestSyscalls syscalls; syscalls.override_write = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UhidGamepad gamepad {fake_fd}; return gamepad.close(); } OperationStatus linux_uhid_read_loop_fake_retry_branches() { LinuxTestSyscalls syscalls; syscalls.override_poll = true; syscalls.poll_results = {-1, 0, 1, 1, 1}; syscalls.poll_revents = {0, 0, 0, POLLIN, POLLIN}; syscalls.poll_errors = {EINTR}; syscalls.override_read = true; syscalls.read_results = {-1, 0}; syscalls.read_errors = {EAGAIN}; return run_fake_uhid_read_loop(syscalls, 5); } OperationStatus linux_uhid_read_loop_fake_poll_errors() { LinuxTestSyscalls syscall_failure; syscall_failure.override_poll = true; syscall_failure.poll_results = {-1}; syscall_failure.poll_errors = {EIO}; if (const auto status = run_fake_uhid_read_loop(syscall_failure, 1); !status.ok()) { return status; } LinuxTestSyscalls event_failure; event_failure.override_poll = true; event_failure.poll_results = {1}; event_failure.poll_revents = {POLLERR}; return run_fake_uhid_read_loop(event_failure, 1); } OperationStatus linux_uhid_read_loop_fake_read_error() { LinuxTestSyscalls syscalls; syscalls.override_poll = true; syscalls.poll_results = {1}; syscalls.poll_revents = {POLLIN}; syscalls.override_read = true; syscalls.read_results = {-1}; syscalls.read_errors = {EIO}; return run_fake_uhid_read_loop(syscalls, 1); } OperationStatus linux_uhid_read_loop_fake_output_without_callback() { LinuxTestSyscalls syscalls; syscalls.override_poll = true; syscalls.poll_results = {1, 1}; syscalls.poll_revents = {POLLIN, POLLIN}; syscalls.override_read = true; syscalls.read_results = {static_cast(sizeof(uhid_event)), 0}; syscalls.read_event.type = UHID_OUTPUT; return run_fake_uhid_read_loop(syscalls, 2); } LinuxLibevdevCreationResult linux_uinput_create_fake_libevdev_device(DeviceType device_type) { return create_fake_libevdev_device(device_type); } LinuxLibevdevCreationResult linux_uinput_create_fake_absolute_mouse_device() { return create_fake_libevdev_device( DeviceType::mouse, keep_fake_libevdev_successful, std::nullopt, UinputMouseDeviceKind::absolute ); } LinuxLibevdevCreationResult linux_uinput_create_fake_gamepad(GamepadProfileKind kind) { return create_fake_libevdev_gamepad(kind); } OperationStatus linux_uinput_create_fake_libevdev_allocation_failure(DeviceType device_type) { return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) { syscalls.libevdev_new_returns_null = true; }) .status; } OperationStatus linux_uinput_create_fake_libevdev_event_type_failure(DeviceType device_type) { return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) { syscalls.fail_libevdev_event_type = true; }) .status; } OperationStatus linux_uinput_create_fake_libevdev_event_code_failure(DeviceType device_type) { return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) { syscalls.fail_libevdev_event_code = true; }) .status; } OperationStatus linux_uinput_create_fake_libevdev_property_failure(DeviceType device_type) { return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) { syscalls.fail_libevdev_property = true; }) .status; } OperationStatus linux_uinput_create_fake_libevdev_create_failure(DeviceType device_type) { return create_fake_libevdev_device(device_type, [](LinuxTestSyscalls &syscalls) { syscalls.fail_libevdev_create = true; }) .status; } OperationStatus linux_uinput_keyboard_submit_fake_write_failure() { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.fail_write_call = 1; syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UinputKeyboard keyboard {fake_fd}; return keyboard.submit({.key_code = 0x41, .pressed = true}); } OperationStatus linux_uinput_keyboard_submit_fake_short_write() { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.short_write_call = 1; syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UinputKeyboard keyboard {fake_fd}; return keyboard.submit({.key_code = 0x41, .pressed = true}); } OperationStatus linux_uinput_keyboard_type_text_fake_success() { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UinputKeyboard keyboard {fake_fd}; return keyboard.type_text({.text = "A"}); } OperationStatus linux_uinput_keyboard_type_text_fake_write_failure(int fail_write_call) { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.fail_write_call = fail_write_call; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UinputKeyboard keyboard {fake_fd}; return keyboard.type_text({.text = "A"}); } OperationStatus linux_uinput_keyboard_close_fake_close_failure() { LinuxTestSyscalls syscalls; syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UinputKeyboard keyboard {fake_fd}; return keyboard.close(); } LinuxInputSubmissionResult linux_uinput_keyboard_held_key_events(std::uint32_t interval_ms) { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.override_ioctl = true; syscalls.override_libevdev = true; syscalls.capture_input_events = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; CreateKeyboardOptions options; options.profile = profiles::keyboard(); options.auto_repeat_interval_ms = interval_ms; const auto fd = open_test_fd(); if (fd < 0) { return {system_error_status(ErrorCode::backend_failure, "failed to open test file descriptor", errno), {}}; } UinputKeyboard keyboard {fd}; auto status = keyboard.create(1, options); if (status.ok()) { status = keyboard.submit({.key_code = 0x46, .pressed = true}); } const auto observation_time = std::chrono::milliseconds {std::max(interval_ms * 2, 20U)}; std::this_thread::sleep_for(observation_time); if (status.ok()) { status = keyboard.submit({.key_code = 0x46, .pressed = false}); } std::this_thread::sleep_for(observation_time); if (const auto close_status = keyboard.close(); status.ok()) { status = close_status; } return {std::move(status), std::move(syscalls.written_input_events)}; } OperationStatus linux_uinput_mouse_submit_fake_write_failure(const MouseEvent &event) { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.fail_write_call = 1; syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UinputMouse mouse {fake_fd, fake_fd}; return mouse.submit(event); } OperationStatus linux_uinput_mouse_submit_fake_short_write(const MouseEvent &event) { LinuxTestSyscalls syscalls; syscalls.override_write = true; syscalls.short_write_call = 1; syscalls.override_ioctl = true; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; UinputMouse mouse {fake_fd, fake_fd}; return mouse.submit(event); } OperationStatus linux_xtest_keyboard_submit_success() { #if defined(LIBVIRTUALHID_HAVE_XTEST) XTestKeyboard keyboard; if (const auto status = keyboard.create(); !status.ok()) { return status; } return keyboard.submit({.key_code = 0x41, .pressed = true}); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_keyboard_submit_compose_fallback() { #if defined(LIBVIRTUALHID_HAVE_XTEST) LinuxTestSyscalls syscalls; syscalls.override_x_keycode = true; syscalls.fail_x_keycode_call = 1; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; XTestKeyboard keyboard; if (const auto status = keyboard.create(); !status.ok()) { return status; } return keyboard.submit({.key_code = 0x5D, .pressed = true}); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_keyboard_submit_invalid() { #if defined(LIBVIRTUALHID_HAVE_XTEST) XTestKeyboard keyboard; if (const auto status = keyboard.create(); !status.ok()) { return status; } return keyboard.submit({.key_code = 0x2C, .pressed = true}); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_keyboard_submit_closed() { #if defined(LIBVIRTUALHID_HAVE_XTEST) XTestKeyboard keyboard; if (const auto status = keyboard.create(); !status.ok()) { return status; } static_cast(keyboard.close()); return keyboard.submit({.key_code = 0x41, .pressed = true}); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_keyboard_type_text_success() { #if defined(LIBVIRTUALHID_HAVE_XTEST) XTestKeyboard keyboard; if (const auto status = keyboard.create(); !status.ok()) { return status; } return keyboard.type_text({.text = "A"}); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_keyboard_type_text_fake_keycode_failure(int fail_keycode_call) { #if defined(LIBVIRTUALHID_HAVE_XTEST) LinuxTestSyscalls syscalls; syscalls.override_x_keycode = true; syscalls.fail_x_keycode_call = fail_keycode_call; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; XTestKeyboard keyboard; if (const auto status = keyboard.create(); !status.ok()) { return status; } return keyboard.type_text({.text = "A"}); #else static_cast(fail_keycode_call); return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_keyboard_create_query_failure() { #if defined(LIBVIRTUALHID_HAVE_XTEST) LinuxTestSyscalls syscalls; syscalls.override_xtest_query = true; syscalls.xtest_query_result = false; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; XTestKeyboard keyboard; return keyboard.create(); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_mouse_submit_success() { #if defined(LIBVIRTUALHID_HAVE_XTEST) XTestMouse mouse; if (const auto status = mouse.create(); !status.ok()) { return status; } using enum MouseEventKind; if (const auto status = mouse.submit({.kind = relative_motion, .x = 1, .y = -1}); !status.ok()) { return status; } if (const auto status = mouse.submit({.kind = absolute_motion, .x = 1, .y = 1, .width = 2, .height = 2}); !status.ok()) { return status; } if (const auto status = mouse.submit({.kind = button, .button = MouseButton::extra, .pressed = true}); !status.ok()) { return status; } if (const auto status = mouse.submit({.kind = vertical_scroll, .high_resolution_scroll = 120}); !status.ok()) { return status; } return mouse.submit({.kind = horizontal_scroll, .high_resolution_scroll = -120}); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_mouse_submit_closed() { #if defined(LIBVIRTUALHID_HAVE_XTEST) XTestMouse mouse; if (const auto status = mouse.create(); !status.ok()) { return status; } static_cast(mouse.close()); return mouse.submit({.kind = MouseEventKind::relative_motion, .x = 1, .y = 1}); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } OperationStatus linux_xtest_mouse_create_query_failure() { #if defined(LIBVIRTUALHID_HAVE_XTEST) LinuxTestSyscalls syscalls; syscalls.override_xtest_query = true; syscalls.xtest_query_result = false; ScopedLinuxTestSyscalls scoped_syscalls {syscalls}; XTestMouse mouse; return mouse.create(); #else return OperationStatus::failure(ErrorCode::backend_unavailable, "XTest fallback is not enabled"); #endif } unsigned long linux_xtest_keysym(KeyboardKeyCode key_code) { #if defined(LIBVIRTUALHID_HAVE_XTEST) return key_code_to_keysym(key_code); #else static_cast(key_code); return 0; #endif } int linux_xtest_mouse_button(MouseButton button) { #if defined(LIBVIRTUALHID_HAVE_XTEST) return mouse_button_to_xtest(button); #else static_cast(button); return 1; #endif } } // namespace lvh::detail::test #endif