diff --git a/.github/vmrun-libusb.sh b/.github/vmrun-libusb.sh new file mode 100644 index 00000000..49a49ee4 --- /dev/null +++ b/.github/vmrun-libusb.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# Guest-side commands for the libusb virtual-device test, run inside a virtme-ng +# VM (see .github/workflows/libusb-vhid-test.yml). Kept as a file so a complex +# command line doesn't have to survive vng's argument parser. +# +# Runs as root in the guest, with the host filesystem mounted, cwd at the +# workspace root (which contains the host-built 'build' tree). +set -x + +# The guest's modules.dep may be trimmed; regenerate it so dummy_hcd / +# raw_gadget (and their dependencies) resolve from the overlaid /lib/modules. +depmod -a || true +modprobe dummy_hcd || true +modprobe raw_gadget || true +ls -l /dev/raw-gadget || true + +ctest --test-dir build --output-on-failure +rc=$? + +echo "=== diag ===" +lsmod | grep -E "raw_gadget|dummy_hcd|udc" || true +ls -l /sys/bus/usb/devices/ 2>/dev/null || true +dmesg | tail -40 || true + +echo "VNG_CTEST_EXIT=${rc}" diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 8f61dc3f..42f0449e 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -44,7 +44,7 @@ jobs: - name: Configure CMake run: | rm -rf build install - cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}" + cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON -DHIDAPI_WITH_TESTS=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}" cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}" cmake -B build/framework -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/framework -DCMAKE_FRAMEWORK=ON -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${NIX_COMPILE_FLAGS}" - name: Build CMake Shared @@ -56,6 +56,14 @@ jobs: - name: Build CMake Framework working-directory: build/framework run: make install + - name: Run virtual-device tests (IOHIDUserDevice self-skips on hosted CI) + working-directory: build/shared + run: | + # The macOS virtual device needs the com.apple.developer.hid.virtual.device + # entitlement and interactive user consent, neither available on a hosted + # runner, so DeviceIO_darwin self-skips (CTest code 77). This still + # verifies the provider builds and the test runs/links. + ASAN_OPTIONS=detect_leaks=0 ctest --output-on-failure - name: Check artifacts uses: andstor/file-existence-action@v2 with: @@ -122,7 +130,7 @@ jobs: - name: Configure CMake run: | rm -rf build install - cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" + cmake -B build/shared -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/shared -DHIDAPI_BUILD_HIDTEST=ON -DHIDAPI_WITH_TESTS=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" cmake -B build/static -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo -DHIDAPI_ENABLE_ASAN=ON -DCMAKE_INSTALL_PREFIX=install/static -DBUILD_SHARED_LIBS=FALSE -DHIDAPI_BUILD_HIDTEST=ON "-DCMAKE_C_FLAGS=${GNU_COMPILE_FLAGS}" - name: Build CMake Shared working-directory: build/shared @@ -130,6 +138,19 @@ jobs: - name: Build CMake Static working-directory: build/static run: make install + - name: Run virtual-device tests (uhid -> hidraw; raw-gadget self-skips) + working-directory: build/shared + run: | + # uhid ships in the 'extra' modules package on the runner kernel. + # Everything here is best-effort: if a virtual device can't be provided + # the matching test self-skips (CTest code 77) instead of failing. + sudo apt-get install -y "linux-modules-extra-$(uname -r)" || true + sudo modprobe uhid || true + ls -l /dev/uhid || echo "/dev/uhid not present" + # Run as root so the test can create /dev/uhid and open the resulting + # /dev/hidrawN. LeakSanitizer off (udev keeps allocations alive at + # exit); ASan use-after-free / overflow detection stays active. + sudo env "ASAN_OPTIONS=detect_leaks=0" ctest --output-on-failure - name: Check artifacts uses: andstor/file-existence-action@v2 with: diff --git a/.github/workflows/libusb-vhid-test.yml b/.github/workflows/libusb-vhid-test.yml new file mode 100644 index 00000000..bfe6d312 --- /dev/null +++ b/.github/workflows/libusb-vhid-test.yml @@ -0,0 +1,89 @@ +name: Linux libusb Virtual HID Device Test (manual) + +# Runs the device-I/O test against the HIDAPI *libusb* backend using a real +# virtual USB HID device (USB Raw Gadget on top of dummy_hcd). +# +# The hosted ubuntu-latest (azure) kernel is built without the USB gadget +# subsystem, so raw_gadget/dummy_hcd can't be loaded (or even built) there. We +# therefore run the test inside a lightweight VM (virtme-ng + QEMU) booting a +# *generic* Ubuntu kernel, whose linux-modules-extra ships dummy_hcd and +# raw_gadget. The VM shares the host filesystem, so it runs the binaries built +# on the host. The same approach works locally and on WSL2 (which also lacks +# those modules in its default kernel). +# +# Runs on demand (workflow_dispatch) or on a PR labelled 'ci-virtual-device'. + +on: + workflow_dispatch: + pull_request: + types: [opened, reopened, labeled, synchronize] + +jobs: + libusb-rawgadget: + if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci-virtual-device') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + path: hidapisrc + + - name: Install deps + a gadget-capable generic kernel + virtme-ng + run: | + set -eux + sudo apt-get update + sudo apt-get install -y \ + build-essential cmake libudev-dev libusb-1.0-0-dev \ + qemu-system-x86 virtme-ng linux-image-generic + # raw_gadget ships in the generic kernel's modules-extra; install that + # plus headers (needed to build dummy_hcd, which Ubuntu doesn't package). + KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1) + echo "generic kernel: ${KVER}" + sudo apt-get install -y "linux-modules-extra-${KVER}" "linux-headers-${KVER}" + find "/lib/modules/${KVER}" \( -name 'raw_gadget*' -o -name 'dummy_hcd*' \) || true + + - name: Build dummy_hcd for the generic kernel (Ubuntu ships no package) + run: | + set -eux + KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1) + KMAJ=$(echo "${KVER}" | grep -oE '^[0-9]+\.[0-9]+') + mkdir -p dummyhcd + # Ubuntu packages no dummy_hcd; build it from the upstream source that + # matches the generic kernel's major version (xairy's copy tracks newer + # kernels and won't compile against an older one). + curl -fsSL -o dummyhcd/dummy_hcd.c \ + "https://raw.githubusercontent.com/torvalds/linux/v${KMAJ}/drivers/usb/gadget/udc/dummy_hcd.c" + printf 'obj-m += dummy_hcd.o\n' > dummyhcd/Makefile + make -C "/lib/modules/${KVER}/build" M="${PWD}/dummyhcd" modules + sudo install -m 0644 "${PWD}/dummyhcd/dummy_hcd.ko" \ + "/lib/modules/${KVER}/kernel/drivers/usb/gadget/udc/" + sudo depmod -a "${KVER}" + + - name: Build HIDAPI + tests (libusb backend) + run: | + cmake -B build -S hidapisrc -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DHIDAPI_WITH_LIBUSB=ON -DHIDAPI_WITH_HIDRAW=OFF -DHIDAPI_WITH_TESTS=ON + cmake --build build + + - name: Run DeviceIO_libusb inside a VM (generic kernel + raw_gadget) + run: | + set -eux + # The generic kernel just installed (has dummy_hcd + raw_gadget modules). + KVER=$(ls -1 /lib/modules | grep -- '-generic$' | sort -V | tail -n1) + KIMG="/boot/vmlinuz-${KVER}" + echo "guest kernel: ${KVER} (${KIMG})" + test -e "${KIMG}" + # Ubuntu installs the kernel image as 0600 root:root, but vng reads it + # as the (non-root) runner user; make it readable. + sudo chmod a+r "${KIMG}" + # Make sure KVM is usable for acceleration (else vng falls back to TCG). + sudo chmod 666 /dev/kvm 2>/dev/null || true + # Boot that kernel in a VM (KVM if /dev/kvm is usable, else TCG). The + # guest runs as root with the host fs mounted; load the gadget modules + # and run the test against the host build tree. vng exit-code + # propagation varies, so derive pass/fail from a sentinel line. + # Boot the kernel in a VM and run the guest script via --exec (vng's + # native flag; passing a complex command after '--' gets mangled). The + # script loads dummy_hcd/raw_gadget and runs the test (see it for why). + vng -v --pwd -r "${KIMG}" --exec "bash hidapisrc/.github/vmrun-libusb.sh" 2>&1 | tee vng.log + echo "--- VM run result ---" + grep -q "VNG_CTEST_EXIT=0" vng.log diff --git a/.github/workflows/win-vhid-test.yml b/.github/workflows/win-vhid-test.yml new file mode 100644 index 00000000..ca003db1 --- /dev/null +++ b/.github/workflows/win-vhid-test.yml @@ -0,0 +1,109 @@ +name: Windows Virtual HID Device Test (manual) + +# Builds, self-signs and installs a modified vhidmini2 UMDF2 driver on a hosted +# windows-latest runner, then runs the backend-agnostic device-I/O test against +# that real virtual HID device (winapi backend). It installs a kernel driver, so +# it is not part of the per-push CI matrix; run it on demand from the Actions +# tab, or by adding the 'ci-virtual-device' label to a pull request. + +on: + workflow_dispatch: + # Also runs automatically on a pull request that carries the + # 'ci-virtual-device' label (the job below is gated on that label). + pull_request: + types: [opened, reopened, labeled, synchronize] + +jobs: + win-vhid: + # workflow_dispatch always runs; on a PR, only when the label is present. + if: github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'ci-virtual-device') + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + # The hosted runner ships the Windows SDK but not the WDK (no UMDF headers), + # so the WDK must be obtained. Cache a self-contained offline installer + # layout (the components, not just the bootstrapper) so it is downloaded only + # once; later runs restore it from the cache and install offline. + - name: Cache the WDK installer layout + id: wdk-cache + uses: actions/cache@v4 + with: + path: wdk-layout + key: wdk-layout-26100.6584 + + - name: Download WDK installer layout (cache miss only) + if: steps.wdk-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/?linkid=2335869" -OutFile "$env:RUNNER_TEMP\wdksetup.exe" + # /layout downloads a complete offline copy into wdk-layout (cached). + Start-Process -FilePath "$env:RUNNER_TEMP\wdksetup.exe" -ArgumentList '/layout', "$PWD\wdk-layout", '/quiet', '/ceip', 'off' -Wait + + - name: Build vhidmini2 (UMDF2) + shell: pwsh + run: | + $proj = "src\tests\windows\driver\VhidminiUm.vcxproj" + $inc = "C:\Program Files (x86)\Windows Kits\10\Include" + # Use a preinstalled WDK if a future image ships one (kit with UMDF + # headers); otherwise install from the cached offline layout. + $ver = Get-ChildItem $inc -Directory -ErrorAction SilentlyContinue | + Where-Object { Test-Path (Join-Path $_.FullName 'wdf\umdf') } | + Sort-Object Name -Descending | Select-Object -First 1 -ExpandProperty Name + if ($ver) { + Write-Host "Using preinstalled WDK ($ver)." + } else { + Write-Host "Installing WDK from the cached offline layout ..." + Start-Process -FilePath "wdk-layout\wdksetup.exe" -ArgumentList '/quiet', '/norestart', '/ceip', 'off' -Wait + $ver = "10.0.26100.0" + } + $vcvars = "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + cmd /c "call `"$vcvars`" && msbuild $proj /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=$ver /v:minimal" + if ($LASTEXITCODE -ne 0) { throw "vhidmini2 build failed (exit $LASTEXITCODE)." } + + - name: Trust the driver's test certificate + shell: pwsh + run: | + $cer = "src/tests/windows/driver/x64/Release/VhidminiUm.cer" + Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\Root | Out-Null + Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPublisher | Out-Null + Write-Host "Imported test cert into Root and TrustedPublisher." + + - name: Install virtual HID device (devcon, root-enumerated) + shell: pwsh + run: | + $devcon = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10" -Recurse -Filter devcon.exe -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 -ExpandProperty FullName + Write-Host "devcon: $devcon" + $inf = (Resolve-Path "src/tests/windows/driver/x64/Release/VhidminiUm/VhidminiUm.inf").Path + Write-Host "inf: $inf" + & $devcon install $inf "root\VhidminiUm" + Write-Host "devcon exit: $LASTEXITCODE" + + - name: HID devices present (diagnostic) + shell: pwsh + run: | + Start-Sleep -Seconds 3 + Get-PnpDevice -Class HIDClass -ErrorAction SilentlyContinue | + Select-Object Status, FriendlyName, InstanceId | Format-Table -AutoSize + + - name: Build HIDAPI + tests + shell: pwsh + run: | + cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DHIDAPI_WITH_TESTS=ON + cmake --build build --config Release + + - name: Run device-I/O test against the virtual device + shell: pwsh + working-directory: build + run: | + ctest -C Release -R DeviceIO_winapi --output-on-failure + + - name: Cleanup virtual device + if: always() + shell: pwsh + run: | + $devcon = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10" -Recurse -Filter devcon.exe -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\x64\\' } | Select-Object -First 1 -ExpandProperty FullName + if ($devcon) { & $devcon remove "root\VhidminiUm" 2>$null } + Write-Host "cleanup done" diff --git a/CMakeLists.txt b/CMakeLists.txt index beca172b..b9758155 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,8 +71,12 @@ if(HIDAPI_ENABLE_ASAN) endif() if(WIN32) - # so far only Windows has tests option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" ${IS_DEBUG_BUILD}) +elseif(CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE) + # Linux and macOS have virtual-device based tests (uhid / raw-gadget / + # IOHIDUserDevice). Off by default: they need a virtual device at runtime + # and otherwise self-skip. + option(HIDAPI_WITH_TESTS "Build HIDAPI (unit-)tests" OFF) else() set(HIDAPI_WITH_TESTS OFF) endif() @@ -92,6 +96,10 @@ if(HIDAPI_BUILD_HIDTEST) add_subdirectory(hidtest) endif() +if(HIDAPI_WITH_TESTS AND (WIN32 OR CMAKE_SYSTEM_NAME MATCHES "Linux" OR APPLE)) + add_subdirectory(src/tests) +endif() + if(HIDAPI_ENABLE_ASAN) if(NOT MSVC) # MSVC doesn't recognize those options, other compilers - requiring it diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt new file mode 100644 index 00000000..f0e666cc --- /dev/null +++ b/src/tests/CMakeLists.txt @@ -0,0 +1,80 @@ +# Backend-generic HIDAPI (unit-)tests, run against a virtual HID device. +# +# The tests are written against the public HIDAPI API and the backend-agnostic +# test_virtual_device interface (test_virtual_device.h), so the same test runs +# against any backend for which a virtual-device provider exists. Each provider +# implements the pre-recorded "scenario" protocol: the test triggers a scenario +# by sending a Feature report, and the device replays a canned input report. +# +# Providers: +# - Linux / hidraw : test_virtual_device_uhid.c (kernel /dev/uhid) +# - Linux / libusb : test_virtual_device_rawgadget.c (/dev/raw-gadget + dummy_hcd) +# - Windows / winapi : test_virtual_device_win.c (modified vhidmini2 UMDF2 driver) +# - macOS / darwin : test_virtual_device_mac.c (IOHIDUserDevice) +# +# The libusb (raw-gadget), Windows and macOS virtual devices need privileged +# out-of-band setup (kernel modules / a signed driver / an entitlement) that is +# only performed by the dedicated CI jobs. Whenever the virtual device cannot be +# created or does not enumerate, the test returns CTest's SKIP code (77) instead +# of failing, so ordinary builds on any host stay green. + +find_package(Threads REQUIRED) + +# Define a device-I/O test built from test_device_io.c + , +# linked against the HIDAPI , and registered with CTest (it self-skips +# via return code 77 when no virtual device is present). +function(hidapi_add_vdev_test name provider backend) + add_executable(${name} test_device_io.c ${provider}) + set_target_properties(${name} PROPERTIES + C_STANDARD 11 + C_STANDARD_REQUIRED TRUE + ) + target_link_libraries(${name} PRIVATE ${backend} Threads::Threads) + if(HIDAPI_ENABLE_ASAN AND NOT MSVC) + target_link_options(${name} PRIVATE -fsanitize=address) + endif() + add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES + SKIP_RETURN_CODE 77 + TIMEOUT 60 + ) +endfunction() + +# --- Linux: hidraw backend via /dev/uhid ----------------------------------- +if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_hidraw) + hidapi_add_vdev_test(DeviceIO_hidraw test_virtual_device_uhid.c hidapi_hidraw) +endif() + +# --- Linux: libusb backend via /dev/raw-gadget (+ dummy_hcd) ---------------- +# Built whenever the libusb backend is, so it keeps compiling; at runtime it +# self-skips unless the raw-gadget virtual device has been set up (CI job). +if(CMAKE_SYSTEM_NAME MATCHES "Linux" AND TARGET hidapi_libusb) + hidapi_add_vdev_test(DeviceIO_libusb test_virtual_device_rawgadget.c hidapi_libusb) +endif() + +# --- Windows: winapi backend via a modified vhidmini2 UMDF driver ----------- +if(WIN32 AND TARGET hidapi_winapi) + hidapi_add_vdev_test(DeviceIO_winapi test_virtual_device_win.c hidapi_winapi) + # HidD_GetPreparsedData / HidP_GetCaps used by the Windows provider. + target_link_libraries(DeviceIO_winapi PRIVATE hid) + # Run from the directory holding the hidapi DLL so a shared build can find + # it at launch (there is no rpath on Windows). + set_tests_properties(DeviceIO_winapi PROPERTIES + WORKING_DIRECTORY "$") + # With ASan (MSVC) the test exe needs the ASan runtime DLL, which lives next + # to the MSVC tools; add it to PATH (CMake >= 3.22). + if(HIDAPI_ENABLE_ASAN AND MSVC AND NOT CMAKE_VERSION VERSION_LESS "3.22") + get_filename_component(MSVC_BUILD_TOOLS_DIR "${CMAKE_LINKER}" DIRECTORY) + set_property(TEST DeviceIO_winapi PROPERTY + ENVIRONMENT_MODIFICATION "PATH=path_list_append:${MSVC_BUILD_TOOLS_DIR}") + endif() +endif() + +# --- macOS: IOKit backend via IOHIDUserDevice ------------------------------ +# Self-skips where the virtual-device entitlement / user consent isn't +# available (e.g. hosted CI runners); usable locally / on a self-hosted Mac. +if(APPLE AND TARGET hidapi_darwin) + hidapi_add_vdev_test(DeviceIO_darwin test_virtual_device_mac.c hidapi_darwin) + target_link_libraries(DeviceIO_darwin PRIVATE + "-framework IOKit" "-framework CoreFoundation") +endif() diff --git a/src/tests/README.md b/src/tests/README.md new file mode 100644 index 00000000..4eeb9fd8 --- /dev/null +++ b/src/tests/README.md @@ -0,0 +1,95 @@ +# HIDAPI virtual-device tests + +Backend-generic HIDAPI tests that run against a **virtual HID device** so they +need no physical hardware. Every test is written purely against the public +HIDAPI API plus the small backend-agnostic `test_virtual_device` interface +(`test_virtual_device.h`), so the *same* test runs against every backend that +provides a virtual-device implementation. + +## Scenario protocol + +Rather than injecting arbitrary input from the test (which would need +platform-specific plumbing), the virtual device has a few **pre-recorded +scenarios** baked in. The test triggers one using the ordinary public API — it +sends a *Feature report* whose first payload byte is a `TEST_VDEV_CMD_*` command +— and the device replays the matching canned *input report*. This keeps the test +code 100% platform-neutral; all device behaviour lives in the per-backend +provider. See `test_virtual_device.h` for the shared contract (report size, +command bytes, expected payloads). + +## Tests + +| Test | What it exercises | +|------|-------------------| +| `test_device_io.c` | open → write an output report → trigger+read input reports (Feature-report write, then input-report read-back) → close | + +## Providers + +| Platform / backend | Provider | Mechanism | CI | +|--------------------|----------|-----------|----| +| Linux / hidraw | `test_virtual_device_uhid.c` | kernel `/dev/uhid` | runs in `builds.yml` (ubuntu-cmake) | +| Linux / libusb | `test_virtual_device_rawgadget.c` | `/dev/raw-gadget` + `dummy_hcd` (in a VM) | builds + self-skips in `builds.yml`; runs in the manual `libusb-vhid-test` job (in a VM) | +| Windows / winapi | `test_virtual_device_win.c` + `windows/driver/` | modified vhidmini2 UMDF2 driver | builds + self-skips in `builds.yml`; runs in the manual `win-vhid-test` job | +| macOS / darwin | `test_virtual_device_mac.c` | `IOHIDUserDevice` (IOKit) | builds + self-skips in `builds.yml` (macos-cmake); runs on a real Mac | + +Whenever a virtual device cannot be created or does not enumerate, the test +returns CTest's **skip** code (77) instead of failing, so ordinary builds on any +host stay green. + +### Why some providers only run in a dedicated job + +Some virtual devices need privileged, out-of-band setup that isn't appropriate +for the per-push CI matrix: + +* **Windows** — the vhidmini2 driver must be built, self-signed and installed + (via `devcon`, no reboot) before the test, and removed afterwards. The + `win-vhid-test` workflow does this end-to-end on a hosted `windows-latest` + runner. The driver under `windows/driver/` is derived from Microsoft's + vhidmini2 sample and is licensed separately under the **MS-PL** (not HIDAPI's + license); see `windows/driver/README.md` and `windows/driver/LICENSE.txt`. +* **Linux / libusb** — needs the `raw_gadget` and `dummy_hcd` kernel modules, + which the hosted `ubuntu-latest` kernel is built *without* (it has no USB + gadget subsystem). The `libusb-vhid-test` workflow therefore runs the test + inside a lightweight VM (`virtme-ng` + QEMU) booting a *generic* Ubuntu kernel + whose `linux-modules-extra` ships both modules; the VM shares the host + filesystem, so it runs the host-built binaries. The same approach works + locally and on WSL2 (whose default kernel also lacks these modules). +* **macOS** — creating an `IOHIDUserDevice` is gated by the + `com.apple.developer.hid.virtual.device` entitlement *and* an interactive + Accessibility (TCC) consent prompt, neither available on a hosted runner, so + the provider self-skips there. It is meant to actually run on a developer + machine or a self-hosted runner where consent has been granted. + +## Platforms without a provider (documented, not implemented) + +Some platforms have no practical way to *create* a virtual HID device — not even +in CI — so there is intentionally no provider for them (the backend simply has +no `DeviceIO_*` test): + +* **FreeBSD** — HIDAPI uses the libusb backend, but FreeBSD has no userspace + USB/HID *creation* facility: `cuse(3)` can't produce a libusb-visible USB + device, `usb_template(4)` ("USB device mode") needs a hardware USB Device + Controller (there is no `dummy_hcd` analogue), and the planned `usrhid(4)` (a + `/dev/uhid` equivalent) is not yet in-tree. A real test would need physical + device-mode hardware or `usrhid`. +* **NetBSD / OpenBSD** — `uhid(4)` is consumer-only (no create/emulate ioctl), + there is no `cuse`, and `rump` has no virtual USB host controller; on OpenBSD + the libusb backend additionally needs `uhid`/`uhidev` disabled in a custom + kernel. No userspace virtual-device path exists. + +CI can build HIDAPI on these systems, but cannot exercise a virtual device, so +the device-I/O test is not wired up there. + +## Running locally + +```sh +# Linux (hidraw via uhid) +cmake -B build -S . -DHIDAPI_WITH_TESTS=ON +cmake --build build +sudo modprobe uhid +sudo ctest --test-dir build -R DeviceIO_hidraw --output-on-failure +``` + +On Windows/macOS configure with `-DHIDAPI_WITH_TESTS=ON` and run `ctest`; the +device-backed tests self-skip unless the corresponding virtual device has been +set up (see the dedicated workflows under `.github/workflows/`). diff --git a/src/tests/test_device_io.c b/src/tests/test_device_io.c new file mode 100644 index 00000000..2c06be7f --- /dev/null +++ b/src/tests/test_device_io.c @@ -0,0 +1,165 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + A simple device-I/O smoke test for the virtual HID device: + open the device, write an output report, exchange a couple of + reports (a Feature report triggers the device to replay a + pre-recorded input report, which is then read back), and close. + + The test is backend-agnostic: it only ever calls the public HIDAPI + API and the test_virtual_device interface, so the very same test + runs against every backend that ships a virtual-device provider + (Linux uhid, Windows vhidmini2, libusb raw-gadget, macOS). + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +#include +#include +#include + +#include + +#include "test_virtual_device.h" + +/* CTest treats this exit code as "skipped" (see SKIP_RETURN_CODE in CMake). */ +#define EXIT_SKIP 77 + +/* Test-unique ids so enumeration cannot collide with real hardware. */ +#define TEST_VID 0xF1D0 +#define TEST_PID 0x9001 +#define TEST_SERIAL "HIDAPI-DEVICE-IO-TEST" + +#define OPEN_TIMEOUT_MS 5000 +#define READ_TIMEOUT_MS 2000 + +static int g_failures = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + printf(" CHECK failed: %s (line %d)\n", \ + #cond, __LINE__); \ + g_failures++; \ + return -1; \ + } \ + } while (0) + +/* Read one full input report (TEST_VDEV_REPORT_SIZE bytes), waiting up to + timeout_ms in total. Returns 0 and fills buf on success, -1 otherwise. */ +static int read_input_report(hid_device *h, unsigned char *buf, int timeout_ms) +{ + int waited = 0; + while (waited < timeout_ms) { + int n = hid_read_timeout(h, buf, TEST_VDEV_REPORT_SIZE, 250); + if (n < 0) + return -1; /* read error */ + if (n >= TEST_VDEV_REPORT_SIZE) + return 0; /* got a full report */ + waited += 250; + } + return -1; /* timed out */ +} + +/* Print a flushed progress marker so a hang is localised on a CTest timeout. */ +static void step(const char *what) +{ + printf(" -> %s\n", what); + fflush(stdout); +} + +/* open -> write -> exchange reports -> (caller closes). */ +static int run_device_io(test_virtual_device *vdev, hid_device *h) +{ + unsigned char out_report[1 + TEST_VDEV_REPORT_SIZE]; + unsigned char in_report[TEST_VDEV_REPORT_SIZE]; + const unsigned char expect_a[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_A; + const unsigned char expect_b[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_B; + + /* write: send one output report (report id 0 + payload). Output-report + support depends on how each virtual device is built, so this is + best-effort - we exercise hid_write() and log the result rather than + hard-failing. The strictly-checked write path below is the Feature + report (the scenario trigger), which every provider implements. */ + memset(out_report, 0, sizeof(out_report)); + out_report[0] = 0x00; /* report id (unnumbered device) */ + out_report[1] = 0x10; + out_report[2] = 0x20; + step("hid_write(output)"); + printf(" hid_write(output) returned %d\n", + hid_write(h, out_report, sizeof(out_report))); + + /* exchange A: a Feature report triggers scenario A; read the replay. */ + step("trigger A (hid_send_feature_report)"); + CHECK(test_virtual_device_trigger(vdev, h, TEST_VDEV_CMD_EMIT_A) >= 0); + step("read A (hid_read_timeout)"); + memset(in_report, 0, sizeof(in_report)); + CHECK(read_input_report(h, in_report, READ_TIMEOUT_MS) == 0); + CHECK(memcmp(in_report, expect_a, TEST_VDEV_REPORT_SIZE) == 0); + + /* exchange B. */ + step("trigger B (hid_send_feature_report)"); + CHECK(test_virtual_device_trigger(vdev, h, TEST_VDEV_CMD_EMIT_B) >= 0); + step("read B (hid_read_timeout)"); + memset(in_report, 0, sizeof(in_report)); + CHECK(read_input_report(h, in_report, READ_TIMEOUT_MS) == 0); + CHECK(memcmp(in_report, expect_b, TEST_VDEV_REPORT_SIZE) == 0); + + step("done"); + return 0; +} + +int main(void) +{ + test_virtual_device *vdev = NULL; + hid_device *h; + int rc; + + if (hid_init() != 0) { + printf("hid_init() failed\n"); + return EXIT_FAILURE; + } + + step("create virtual device"); + rc = test_virtual_device_create(&vdev, TEST_VID, TEST_PID, TEST_SERIAL); + if (rc == TEST_VDEV_UNAVAILABLE) { + printf("virtual device unavailable on this host - skipping\n"); + hid_exit(); + return EXIT_SKIP; + } + if (rc != TEST_VDEV_OK || !vdev) { + printf("failed to create virtual device (rc=%d)\n", rc); + hid_exit(); + return EXIT_FAILURE; + } + + /* Opening doubles as the presence probe: on platforms where create() + cannot itself tell whether a device is installed (Windows, macOS), + a failure to enumerate/open means there is no device here, so the + test skips rather than fails. */ + step("open via HIDAPI"); + h = test_virtual_device_open_hidapi(vdev, OPEN_TIMEOUT_MS); + if (!h) { + printf("virtual device did not enumerate - skipping\n"); + test_virtual_device_destroy(vdev); + hid_exit(); + return EXIT_SKIP; + } + + printf("running device-io smoke test...\n"); + rc = run_device_io(vdev, h); + printf("%s device_io\n", (rc == 0 && g_failures == 0) ? "PASS" : "FAIL"); + + hid_close(h); + test_virtual_device_destroy(vdev); + hid_exit(); + + return (g_failures == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/tests/test_platform.h b/src/tests/test_platform.h new file mode 100644 index 00000000..34609e2e --- /dev/null +++ b/src/tests/test_platform.h @@ -0,0 +1,119 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Test support: tiny cross-platform helpers (threads, timing) + so the HIDAPI unit tests stay platform-neutral. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +#ifndef HIDAPI_TEST_PLATFORM_H__ +#define HIDAPI_TEST_PLATFORM_H__ + +#ifdef _WIN32 + #include +#else + #ifndef _GNU_SOURCE + #define _GNU_SOURCE /* for pthread_timedjoin_np */ + #endif + #include + #include +#endif + +/* Monotonic milliseconds for measuring elapsed time. */ +static long long test_now_ms(void) +{ +#ifdef _WIN32 + return (long long)GetTickCount64(); +#else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +#endif +} + +static void test_sleep_ms(int ms) +{ +#ifdef _WIN32 + Sleep((DWORD)ms); +#else + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (long)(ms % 1000) * 1000000L; + nanosleep(&ts, NULL); +#endif +} + +/* A joinable thread running void fn(void*). Results are communicated through + * the user's arg (this matches how the tests use a context struct). */ +typedef struct test_thread { + void (*fn)(void *); + void *arg; +#ifdef _WIN32 + HANDLE handle; +#else + pthread_t thread; +#endif +} test_thread; + +#ifdef _WIN32 +static DWORD WINAPI test__thread_entry(LPVOID p) +{ + test_thread *t = (test_thread *)p; + t->fn(t->arg); + return 0; +} +#else +static void *test__thread_entry(void *p) +{ + test_thread *t = (test_thread *)p; + t->fn(t->arg); + return NULL; +} +#endif + +/* Returns 0 on success, -1 on failure. */ +static int test_thread_start(test_thread *t, void (*fn)(void *), void *arg) +{ + t->fn = fn; + t->arg = arg; +#ifdef _WIN32 + t->handle = CreateThread(NULL, 0, test__thread_entry, t, 0, NULL); + return t->handle ? 0 : -1; +#else + return pthread_create(&t->thread, NULL, test__thread_entry, t) == 0 ? 0 : -1; +#endif +} + +/* Join with a timeout. Returns 0 if the thread finished, -1 on timeout. */ +static int test_thread_join_timeout(test_thread *t, int timeout_ms) +{ +#ifdef _WIN32 + DWORD r = WaitForSingleObject(t->handle, (DWORD)timeout_ms); + if (r == WAIT_OBJECT_0) { + CloseHandle(t->handle); + t->handle = NULL; + return 0; + } + return -1; +#else + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += timeout_ms / 1000; + ts.tv_nsec += (long)(timeout_ms % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec += 1; + ts.tv_nsec -= 1000000000L; + } + return pthread_timedjoin_np(t->thread, NULL, &ts) == 0 ? 0 : -1; +#endif +} + +#endif /* HIDAPI_TEST_PLATFORM_H__ */ diff --git a/src/tests/test_virtual_device.h b/src/tests/test_virtual_device.h new file mode 100644 index 00000000..c326c231 --- /dev/null +++ b/src/tests/test_virtual_device.h @@ -0,0 +1,114 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Test support: an opaque "virtual HID device" used by the + HIDAPI unit tests. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +/* + * A backend-agnostic virtual HID device for the unit tests. The device has a + * fixed report descriptor (one Input, Output and Feature report of + * TEST_VDEV_REPORT_SIZE bytes, no Report ID) and a small set of *pre-recorded + * scenarios* baked into the device itself. + * + * Rather than letting the test inject arbitrary input (which would require + * platform-specific plumbing in the test), the test triggers a scenario using + * the ordinary public HIDAPI API - it sends a Feature report whose first byte + * is a TEST_VDEV_CMD_* command - and the device replays the corresponding + * pre-recorded input report(s). This keeps the test code 100% platform-neutral + * (it only ever calls public hid_*() functions); all device behaviour lives in + * the per-backend provider: + * + * - Linux: test_virtual_device_uhid.c (kernel /dev/uhid -> hidraw) + * - Windows: test_virtual_device_win.c (modified vhidmini2 UMDF driver) + * - others: (future) + * + * The provider only needs to implement create / open / destroy; the scenario + * playback is part of the virtual device (the uhid event pump, or the driver). + */ + +#ifndef HIDAPI_TEST_VIRTUAL_DEVICE_H__ +#define HIDAPI_TEST_VIRTUAL_DEVICE_H__ + +#include +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* test_virtual_device_create() return codes. */ +#define TEST_VDEV_OK 0 +#define TEST_VDEV_ERROR (-1) +#define TEST_VDEV_UNAVAILABLE (-2) /* backend can't create a device here -> skip */ + +/* Input/Output/Feature report payload size of the virtual device. */ +#define TEST_VDEV_REPORT_SIZE 8 + +/* + * Scenario commands. The test selects a scenario by sending a Feature report + * whose first payload byte is one of these. The device then replays the + * matching pre-recorded input report(s). These values, and the payloads below, + * are the contract shared between the test and every provider (including the + * Windows driver, which hard-codes the same constants). + */ +#define TEST_VDEV_CMD_NONE 0x00 +#define TEST_VDEV_CMD_EMIT_A 0x01 /* device emits TEST_VDEV_INPUT_A once */ +#define TEST_VDEV_CMD_EMIT_B 0x02 /* device emits TEST_VDEV_INPUT_B once */ + +/* Pre-recorded input report payloads (each TEST_VDEV_REPORT_SIZE bytes). */ +#define TEST_VDEV_INPUT_A { 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8 } +#define TEST_VDEV_INPUT_B { 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8 } + +/* Opaque virtual HID device; the provider owns the concrete struct. */ +typedef struct test_virtual_device test_virtual_device; + +/* + * Create a virtual HID device discoverable by HIDAPI with the given ids and + * serial. On success *out_dev receives the device and TEST_VDEV_OK is returned. + * Returns TEST_VDEV_UNAVAILABLE when the platform's virtual-device mechanism + * isn't usable (missing kernel support, insufficient privileges, ...) so the + * caller can report the test as skipped, or TEST_VDEV_ERROR on a hard failure. + */ +int test_virtual_device_create(test_virtual_device **out_dev, + unsigned short vendor_id, + unsigned short product_id, + const char *serial); + +/* + * Wait (up to timeout_ms) for the virtual device to appear in HIDAPI + * enumeration, then hid_open() it. Returns NULL on timeout/failure. + */ +hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms); + +/* Destroy the virtual device and free all resources. */ +void test_virtual_device_destroy(test_virtual_device *dev); + +/* + * Trigger a pre-recorded scenario on the device by sending the given command + * as the first byte of a Feature report, using the ordinary public HIDAPI + * hid_send_feature_report(). The exact wire length of a feature report is + * platform-specific (Windows requires the buffer to be exactly the device's + * FeatureReportByteLength), so this is implemented per-provider. + * + * Returns >= 0 on success, -1 on error. + */ +int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, + unsigned char command); + +#ifdef __cplusplus +} +#endif + +#endif /* HIDAPI_TEST_VIRTUAL_DEVICE_H__ */ diff --git a/src/tests/test_virtual_device_mac.c b/src/tests/test_virtual_device_mac.c new file mode 100644 index 00000000..4e0678b0 --- /dev/null +++ b/src/tests/test_virtual_device_mac.c @@ -0,0 +1,396 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + macOS implementation of the virtual HID device test interface, + backed by IOHIDUserDevice (IOKit). + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +/* + * IOHIDUserDevice is the userspace IOKit facility that creates a virtual HID + * device visible through IOHIDManager - and therefore to the HIDAPI macOS + * (darwin) backend - without a kext or DriverKit extension. It implements the + * same pre-recorded "scenario" protocol as the other providers + * (see test_virtual_device.h): a Feature SET_REPORT whose first payload byte is + * a TEST_VDEV_CMD_* command makes the device replay the matching canned input + * report via IOHIDUserDeviceHandleReport(). + * + * Important: creating an IOHIDUserDevice is gated on macOS by the + * com.apple.developer.hid.virtual.device entitlement and an interactive + * Accessibility (TCC) consent prompt, neither of which can be satisfied + * head-less. So on hosted CI this provider returns TEST_VDEV_UNAVAILABLE and + * the test is skipped; it is meant to actually run on a developer machine or a + * self-hosted runner where the consent has been granted. + * + * The IOHIDUserDevice* symbols are IOKit SPI (not in the public SDK), so they + * are resolved at run time with dlsym(); if they are missing the provider also + * self-skips rather than failing to build/link. + */ + +#include "test_virtual_device.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/* ---- IOHIDUserDevice SPI (resolved via dlsym) ---------------------------- */ +typedef struct __IOHIDUserDevice *IOHIDUserDeviceRef; +typedef IOReturn (*IOHIDUserDeviceReportCallback)(void *refcon, + IOHIDReportType type, + uint32_t reportID, + uint8_t *report, + CFIndex reportLength); + +typedef IOHIDUserDeviceRef (*fn_create)(CFAllocatorRef, CFDictionaryRef); +typedef void (*fn_schedule)(IOHIDUserDeviceRef, CFRunLoopRef, CFStringRef); +typedef void (*fn_unschedule)(IOHIDUserDeviceRef, CFRunLoopRef, CFStringRef); +typedef void (*fn_reg_cb)(IOHIDUserDeviceRef, IOHIDUserDeviceReportCallback, void *); +typedef IOReturn (*fn_handle)(IOHIDUserDeviceRef, uint8_t *, CFIndex); + +struct mac_spi { + fn_create create; + fn_schedule schedule; + fn_unschedule unschedule; + fn_reg_cb reg_get; + fn_reg_cb reg_set; + fn_handle handle_report; +}; + +/* The same vendor-defined report descriptor as the other providers: one 8-byte + * Input, Output and Feature report, no Report ID. */ +static const unsigned char k_report_descriptor[] = { + 0x06, 0x00, 0xFF, /* Usage Page (Vendor Defined 0xFF00) */ + 0x09, 0x01, /* Usage (0x01) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x09, 0x01, /* Usage (0x01) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x08, /* Report Count (8) */ + 0x81, 0x02, /* Input (Data,Var,Abs) */ + 0x09, 0x01, /* Usage (0x01) */ + 0x91, 0x02, /* Output (Data,Var,Abs) */ + 0x09, 0x01, /* Usage (0x01) */ + 0xB1, 0x02, /* Feature (Data,Var,Abs) */ + 0xC0 /* End Collection */ +}; + +static const unsigned char k_input_a[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_A; +static const unsigned char k_input_b[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_B; + +struct test_virtual_device { + struct mac_spi spi; + void *iokit_handle; /* dlopen handle */ + IOHIDUserDeviceRef device; + + pthread_t runloop_thread; + int thread_started; + CFRunLoopRef runloop; + + pthread_mutex_t lock; + pthread_cond_t cond; + int ready; /* run loop scheduled and running */ + + unsigned short vendor_id; + unsigned short product_id; + char serial[64]; +}; + +static int load_spi(struct mac_spi *spi, void **out_handle) +{ + void *h = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", + RTLD_LAZY | RTLD_LOCAL); + if (!h) + return -1; + + /* dlsym() returns an object pointer; converting it to a function pointer + directly is undefined in ISO C (and -pedantic rejects it), so pun via a + union. */ +#define LOAD_SYM(field, type, name) \ + do { \ + union { void *obj; type fn; } _u; \ + _u.obj = dlsym(h, (name)); \ + (field) = _u.fn; \ + } while (0) + + LOAD_SYM(spi->create, fn_create, "IOHIDUserDeviceCreate"); + LOAD_SYM(spi->schedule, fn_schedule, "IOHIDUserDeviceScheduleWithRunLoop"); + LOAD_SYM(spi->unschedule, fn_unschedule, "IOHIDUserDeviceUnscheduleFromRunLoop"); + LOAD_SYM(spi->reg_get, fn_reg_cb, "IOHIDUserDeviceRegisterGetReportCallback"); + LOAD_SYM(spi->reg_set, fn_reg_cb, "IOHIDUserDeviceRegisterSetReportCallback"); + LOAD_SYM(spi->handle_report, fn_handle, "IOHIDUserDeviceHandleReport"); + +#undef LOAD_SYM + + if (!spi->create || !spi->schedule || !spi->reg_set || !spi->handle_report) { + dlclose(h); + return -1; + } + *out_handle = h; + return 0; +} + +/* SET_REPORT callback == the scenario trigger. */ +static IOReturn set_report_cb(void *refcon, IOHIDReportType type, + uint32_t reportID, uint8_t *report, + CFIndex reportLength) +{ + struct test_virtual_device *dev = (struct test_virtual_device *)refcon; + unsigned char command = TEST_VDEV_CMD_NONE; + CFIndex scan = reportLength > 4 ? 4 : reportLength; + CFIndex i; + (void)type; + (void)reportID; + + for (i = 0; i < scan; i++) { + if (report[i] == TEST_VDEV_CMD_EMIT_A || report[i] == TEST_VDEV_CMD_EMIT_B) { + command = report[i]; + break; + } + } + + if (command == TEST_VDEV_CMD_EMIT_A) { + uint8_t buf[TEST_VDEV_REPORT_SIZE]; + memcpy(buf, k_input_a, sizeof(buf)); + dev->spi.handle_report(dev->device, buf, sizeof(buf)); + } else if (command == TEST_VDEV_CMD_EMIT_B) { + uint8_t buf[TEST_VDEV_REPORT_SIZE]; + memcpy(buf, k_input_b, sizeof(buf)); + dev->spi.handle_report(dev->device, buf, sizeof(buf)); + } + return kIOReturnSuccess; +} + +/* GET_REPORT callback: benign (zero-filled). */ +static IOReturn get_report_cb(void *refcon, IOHIDReportType type, + uint32_t reportID, uint8_t *report, + CFIndex reportLength) +{ + (void)refcon; + (void)type; + (void)reportID; + if (report && reportLength > 0) + memset(report, 0, reportLength); + return kIOReturnSuccess; +} + +static void *runloop_thread_fn(void *arg) +{ + struct test_virtual_device *dev = (struct test_virtual_device *)arg; + + dev->runloop = CFRunLoopGetCurrent(); + dev->spi.schedule(dev->device, dev->runloop, kCFRunLoopDefaultMode); + + pthread_mutex_lock(&dev->lock); + dev->ready = 1; + pthread_cond_signal(&dev->cond); + pthread_mutex_unlock(&dev->lock); + + CFRunLoopRun(); + + if (dev->spi.unschedule) + dev->spi.unschedule(dev->device, dev->runloop, kCFRunLoopDefaultMode); + return NULL; +} + +static void dict_set_number(CFMutableDictionaryRef d, CFStringRef key, int value) +{ + CFNumberRef n = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &value); + if (n) { + CFDictionarySetValue(d, key, n); + CFRelease(n); + } +} + +int test_virtual_device_create(test_virtual_device **out_dev, + unsigned short vendor_id, + unsigned short product_id, + const char *serial) +{ + struct test_virtual_device *dev; + CFMutableDictionaryRef props; + CFDataRef rd; + CFStringRef serial_cf; + int rc; + + if (!out_dev) + return TEST_VDEV_ERROR; + *out_dev = NULL; + + dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); + if (!dev) + return TEST_VDEV_ERROR; + + dev->vendor_id = vendor_id; + dev->product_id = product_id; + snprintf(dev->serial, sizeof(dev->serial), "%s", serial ? serial : ""); + pthread_mutex_init(&dev->lock, NULL); + pthread_cond_init(&dev->cond, NULL); + + if (load_spi(&dev->spi, &dev->iokit_handle) != 0) { + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return TEST_VDEV_UNAVAILABLE; /* SPI not present -> skip */ + } + + props = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + rd = CFDataCreate(kCFAllocatorDefault, k_report_descriptor, + sizeof(k_report_descriptor)); + serial_cf = CFStringCreateWithCString(kCFAllocatorDefault, dev->serial, + kCFStringEncodingUTF8); + if (!props || !rd || !serial_cf) { + if (props) CFRelease(props); + if (rd) CFRelease(rd); + if (serial_cf) CFRelease(serial_cf); + dlclose(dev->iokit_handle); + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return TEST_VDEV_ERROR; + } + + CFDictionarySetValue(props, CFSTR("ReportDescriptor"), rd); + dict_set_number(props, CFSTR(kIOHIDVendorIDKey), vendor_id); + dict_set_number(props, CFSTR(kIOHIDProductIDKey), product_id); + CFDictionarySetValue(props, CFSTR(kIOHIDSerialNumberKey), serial_cf); + CFDictionarySetValue(props, CFSTR(kIOHIDProductKey), + CFSTR("HIDAPI Test Device")); + + dev->device = dev->spi.create(kCFAllocatorDefault, props); + + CFRelease(props); + CFRelease(rd); + CFRelease(serial_cf); + + if (!dev->device) { + /* Entitlement / consent denied (the usual case on hosted CI). */ + dlclose(dev->iokit_handle); + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return TEST_VDEV_UNAVAILABLE; + } + + dev->spi.reg_set(dev->device, set_report_cb, dev); + if (dev->spi.reg_get) + dev->spi.reg_get(dev->device, get_report_cb, dev); + + rc = pthread_create(&dev->runloop_thread, NULL, runloop_thread_fn, dev); + if (rc != 0) { + CFRelease(dev->device); + dlclose(dev->iokit_handle); + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return TEST_VDEV_ERROR; + } + dev->thread_started = 1; + + /* Wait until the run loop has scheduled the device. */ + pthread_mutex_lock(&dev->lock); + while (!dev->ready) + pthread_cond_wait(&dev->cond, &dev->lock); + pthread_mutex_unlock(&dev->lock); + + *out_dev = dev; + return TEST_VDEV_OK; +} + +hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms) +{ + wchar_t wserial[64]; + int waited = 0; + size_t i; + + if (!dev) + return NULL; + + for (i = 0; i + 1 < (sizeof(wserial) / sizeof(wserial[0])) && dev->serial[i]; i++) + wserial[i] = (wchar_t)(unsigned char)dev->serial[i]; + wserial[i] = L'\0'; + + for (;;) { + struct hid_device_info *infos = hid_enumerate(dev->vendor_id, dev->product_id); + struct hid_device_info *cur; + struct hid_device_info *first = NULL; + struct hid_device_info *match = NULL; + hid_device *h = NULL; + + for (cur = infos; cur; cur = cur->next) { + if (!first) + first = cur; + if (cur->serial_number && wcscmp(cur->serial_number, wserial) == 0) { + match = cur; + break; + } + } + if (match || first) + h = hid_open_path((match ? match : first)->path); + hid_free_enumeration(infos); + if (h) + return h; + + if (waited >= timeout_ms) + return NULL; + struct timespec ts = { 0, 50 * 1000000L }; + nanosleep(&ts, NULL); + waited += 50; + } +} + +int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, + unsigned char command) +{ + unsigned char feature[1 + TEST_VDEV_REPORT_SIZE]; + (void)dev; + memset(feature, 0, sizeof(feature)); + feature[0] = 0x00; /* Report ID (the device has no numbered reports) */ + feature[1] = command; /* scenario command, first byte of the payload */ + return hid_send_feature_report(handle, feature, sizeof(feature)); +} + +void test_virtual_device_destroy(test_virtual_device *dev) +{ + if (!dev) + return; + + if (dev->thread_started) { + if (dev->runloop) + CFRunLoopStop(dev->runloop); + pthread_join(dev->runloop_thread, NULL); + dev->thread_started = 0; + } + + if (dev->device) { + CFRelease(dev->device); + dev->device = NULL; + } + if (dev->iokit_handle) { + dlclose(dev->iokit_handle); + dev->iokit_handle = NULL; + } + + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); +} diff --git a/src/tests/test_virtual_device_rawgadget.c b/src/tests/test_virtual_device_rawgadget.c new file mode 100644 index 00000000..5b2851c1 --- /dev/null +++ b/src/tests/test_virtual_device_rawgadget.c @@ -0,0 +1,810 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Linux libusb-backend implementation of the virtual HID device test + interface, backed by the kernel's USB Raw Gadget (/dev/raw-gadget) + on top of the dummy_hcd virtual UDC/HCD. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +/* + * Raw Gadget lets a userspace process *be* a USB device: it answers every EP0 + * control request and drives the endpoints itself. Combined with dummy_hcd + * (which provides a virtual UDC and a virtual host controller on the same + * machine), this enumerates a real virtual USB HID device that the HIDAPI + * libusb backend can open - exercising the USB transfer paths that the hidraw + * backend never touches. + * + * This provider implements the same pre-recorded "scenario" protocol as the + * other providers (see test_virtual_device.h): a Feature SET_REPORT whose first + * payload byte is a TEST_VDEV_CMD_* command makes the device send the matching + * canned input report over its interrupt IN endpoint. + * + * Requirements: the 'raw_gadget' and 'dummy_hcd' kernel modules and root (to + * open /dev/raw-gadget). When unavailable, create() returns + * TEST_VDEV_UNAVAILABLE so the test is skipped rather than failed. + * + * Note: this is little-endian oriented (multi-byte USB descriptor fields are + * assigned directly); it is meant for CI on common little-endian hosts. + */ + +#include "test_virtual_device.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/* ---- Vendored subset of (kernel >= 5.7) ---------- */ +/* Vendored so this file builds even where the UAPI header is absent. The + * structs use C99 flexible array members so their sizeof() (and thus the ioctl + * command numbers) match the kernel's. */ + +#define UDC_NAME_LENGTH_MAX 128 + +struct usb_raw_init { + __u8 driver_name[UDC_NAME_LENGTH_MAX]; + __u8 device_name[UDC_NAME_LENGTH_MAX]; + __u8 speed; +}; + +enum usb_raw_event_type { + USB_RAW_EVENT_INVALID = 0, + USB_RAW_EVENT_CONNECT = 1, + USB_RAW_EVENT_CONTROL = 2 +}; + +struct usb_raw_event { + __u32 type; + __u32 length; + __u8 data[]; +}; + +struct usb_raw_ep_io { + __u16 ep; + __u16 flags; + __u32 length; + __u8 data[]; +}; + +#define USB_RAW_EPS_NUM_MAX 30 +#define USB_RAW_EP_NAME_MAX 16 +#define USB_RAW_EP_ADDR_ANY 0xff + +struct usb_raw_ep_caps { + unsigned int type_control : 1; + unsigned int type_iso : 1; + unsigned int type_bulk : 1; + unsigned int type_int : 1; + unsigned int dir_in : 1; + unsigned int dir_out : 1; +}; + +struct usb_raw_ep_limits { + __u16 maxpacket_limit; + __u16 max_streams; + __u32 reserved; +}; + +struct usb_raw_ep_info { + __u8 name[USB_RAW_EP_NAME_MAX]; + __u32 addr; + struct usb_raw_ep_caps caps; + struct usb_raw_ep_limits limits; +}; + +struct usb_raw_eps_info { + struct usb_raw_ep_info eps[USB_RAW_EPS_NUM_MAX]; +}; + +#define USB_RAW_IOCTL_INIT _IOW('U', 0, struct usb_raw_init) +#define USB_RAW_IOCTL_RUN _IO('U', 1) +#define USB_RAW_IOCTL_EVENT_FETCH _IOR('U', 2, struct usb_raw_event) +#define USB_RAW_IOCTL_EP0_WRITE _IOW('U', 3, struct usb_raw_ep_io) +#define USB_RAW_IOCTL_EP0_READ _IOWR('U', 4, struct usb_raw_ep_io) +#define USB_RAW_IOCTL_EP_ENABLE _IOW('U', 5, struct usb_endpoint_descriptor) +#define USB_RAW_IOCTL_EP_DISABLE _IOW('U', 6, __u32) +#define USB_RAW_IOCTL_EP_WRITE _IOW('U', 7, struct usb_raw_ep_io) +#define USB_RAW_IOCTL_EP_READ _IOWR('U', 8, struct usb_raw_ep_io) +#define USB_RAW_IOCTL_CONFIGURE _IO('U', 9) +#define USB_RAW_IOCTL_VBUS_DRAW _IOW('U', 10, __u32) +#define USB_RAW_IOCTL_EPS_INFO _IOR('U', 11, struct usb_raw_eps_info) +#define USB_RAW_IOCTL_EP0_STALL _IO('U', 12) + +/* ---- HID class descriptor (not in ch9.h) --------------------------------- */ + +struct hid_class_desc { + __u8 bLength; + __u8 bDescriptorType; /* 0x21 HID */ + __u16 bcdHID; + __u8 bCountryCode; + __u8 bNumDescriptors; + __u8 bReportType; /* 0x22 Report */ + __u16 wReportLength; +} __attribute__((packed)); + +#define HID_DT_HID 0x21 +#define HID_DT_REPORT 0x22 + +#define HID_REQ_GET_REPORT 0x01 +#define HID_REQ_SET_REPORT 0x09 + +/* The same vendor-defined report descriptor as the uhid provider: one 8-byte + * Input, Output and Feature report, no Report ID. */ +static const unsigned char k_report_descriptor[] = { + 0x06, 0x00, 0xFF, /* Usage Page (Vendor Defined 0xFF00) */ + 0x09, 0x01, /* Usage (0x01) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x09, 0x01, /* Usage (0x01) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x08, /* Report Count (8) */ + 0x81, 0x02, /* Input (Data,Var,Abs) */ + 0x09, 0x01, /* Usage (0x01) */ + 0x91, 0x02, /* Output (Data,Var,Abs) */ + 0x09, 0x01, /* Usage (0x01) */ + 0xB1, 0x02, /* Feature (Data,Var,Abs) */ + 0xC0 /* End Collection */ +}; + +static const unsigned char k_input_a[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_A; +static const unsigned char k_input_b[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_B; + +struct test_virtual_device { + int fd; /* /dev/raw-gadget */ + pthread_t ep0_thread; + pthread_t int_in_thread; + int ep0_started; + int int_in_started; + volatile int stop; + volatile int ep0_exited; /* ep0 thread has left its fetch loop */ + + volatile int configured; /* SET_CONFIGURATION seen, IN ep enabled */ + int int_in_ep; /* raw-gadget handle for the IN endpoint */ + __u8 int_in_addr; /* bEndpointAddress chosen from EPS_INFO */ + + pthread_mutex_t lock; + pthread_cond_t cond; + volatile unsigned char pending; /* TEST_VDEV_CMD_* to replay, or NONE */ + + unsigned short vendor_id; + unsigned short product_id; + char serial[64]; +}; + +static void sleep_ms(int ms) +{ + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (long)(ms % 1000) * 1000000L; + nanosleep(&ts, NULL); +} + +/* SIGUSR1 has a no-op handler installed WITHOUT SA_RESTART so that sending it + to a worker thread interrupts a blocking raw-gadget ioctl (EVENT_FETCH / + EP_WRITE) with EINTR, letting the thread observe 'stop' and exit at teardown. */ +static void rg_sig_noop(int sig) +{ + (void)sig; +} + +static void rg_install_signal(void) +{ + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = rg_sig_noop; + sigaction(SIGUSR1, &sa, NULL); +} + +/* EP0/EP I/O via a heap buffer sized for the flexible-array struct (heap memory + * has no declared type, so this is alignment- and aliasing-safe). */ +static int ep_io_write(int fd, unsigned long request, int ep, + const void *data, int len) +{ + struct usb_raw_ep_io *io; + int rv; + if (len < 0) + len = 0; + io = (struct usb_raw_ep_io *)calloc(1, sizeof(*io) + (size_t)len); + if (!io) + return -1; + io->ep = (__u16)ep; + io->flags = 0; + io->length = (__u32)len; + if (data && len) + memcpy(io->data, data, (size_t)len); + rv = ioctl(fd, request, io); + free(io); + return rv; +} + +static int ep0_write(int fd, const void *data, int len) +{ + return ep_io_write(fd, USB_RAW_IOCTL_EP0_WRITE, 0, data, len); +} + +static int ep0_read(int fd, void *data, int maxlen) +{ + struct usb_raw_ep_io *io; + int rv; + if (maxlen < 0) + maxlen = 0; + io = (struct usb_raw_ep_io *)calloc(1, sizeof(*io) + (size_t)maxlen); + if (!io) + return -1; + io->ep = 0; + io->flags = 0; + io->length = (__u32)maxlen; + rv = ioctl(fd, USB_RAW_IOCTL_EP0_READ, io); + if (rv > 0 && data) { + int cp = (rv > maxlen) ? maxlen : rv; + memcpy(data, io->data, (size_t)cp); + } + free(io); + return rv; +} + +/* Acknowledge a control transfer that has no data stage. raw-gadget completes + the status stage with an EP0 transfer in the request's direction: EP0_WRITE + for IN requests, EP0_READ for OUT requests (e.g. SET_CONFIGURATION). Using + the wrong one leaves the transfer incomplete and the host times out. */ +static void ep0_ack(int fd, const struct usb_ctrlrequest *ctrl) +{ + if (ctrl->bRequestType & USB_DIR_IN) + ep0_write(fd, NULL, 0); + else + ep0_read(fd, NULL, 0); +} + +/* Build the full configuration descriptor (config + interface + HID + ep). */ +static int build_config_descriptor(unsigned char *buf, int buflen, __u8 ep_addr) +{ + struct usb_config_descriptor cfg; + struct usb_interface_descriptor intf; + struct hid_class_desc hid; + struct usb_endpoint_descriptor ep; + /* On-the-wire sizes: struct usb_endpoint_descriptor is 9 bytes (it carries + 2 trailing audio-only fields), but a non-audio endpoint descriptor is 7 + bytes (USB_DT_ENDPOINT_SIZE). Use the wire sizes so no stray bytes follow + a descriptor (which the host would parse as a bogus length-0 descriptor). */ + int total = USB_DT_CONFIG_SIZE + USB_DT_INTERFACE_SIZE + + (int)sizeof(hid) + USB_DT_ENDPOINT_SIZE; + int off = 0; + + if (buflen < total) + return -1; + + memset(&cfg, 0, sizeof(cfg)); + cfg.bLength = USB_DT_CONFIG_SIZE; + cfg.bDescriptorType = USB_DT_CONFIG; + cfg.wTotalLength = (__u16)total; + cfg.bNumInterfaces = 1; + cfg.bConfigurationValue = 1; + cfg.iConfiguration = 0; + cfg.bmAttributes = 0x80; /* bus powered */ + cfg.bMaxPower = 50; /* 100 mA */ + + memset(&intf, 0, sizeof(intf)); + intf.bLength = USB_DT_INTERFACE_SIZE; + intf.bDescriptorType = USB_DT_INTERFACE; + intf.bInterfaceNumber = 0; + intf.bAlternateSetting = 0; + intf.bNumEndpoints = 1; + intf.bInterfaceClass = USB_CLASS_HID; /* 0x03 */ + intf.bInterfaceSubClass = 0; + intf.bInterfaceProtocol = 0; + intf.iInterface = 0; + + memset(&hid, 0, sizeof(hid)); + hid.bLength = (__u8)sizeof(hid); + hid.bDescriptorType = HID_DT_HID; + hid.bcdHID = 0x0111; + hid.bCountryCode = 0; + hid.bNumDescriptors = 1; + hid.bReportType = HID_DT_REPORT; + hid.wReportLength = (__u16)sizeof(k_report_descriptor); + + memset(&ep, 0, sizeof(ep)); + ep.bLength = USB_DT_ENDPOINT_SIZE; + ep.bDescriptorType = USB_DT_ENDPOINT; + ep.bEndpointAddress = ep_addr; + ep.bmAttributes = USB_ENDPOINT_XFER_INT; /* 0x03 */ + ep.wMaxPacketSize = TEST_VDEV_REPORT_SIZE; + ep.bInterval = 5; + + memcpy(buf + off, &cfg, USB_DT_CONFIG_SIZE); off += USB_DT_CONFIG_SIZE; + memcpy(buf + off, &intf, USB_DT_INTERFACE_SIZE); off += USB_DT_INTERFACE_SIZE; + memcpy(buf + off, &hid, sizeof(hid)); off += (int)sizeof(hid); + memcpy(buf + off, &ep, USB_DT_ENDPOINT_SIZE); + return total; +} + +static int build_device_descriptor(struct test_virtual_device *dev, + struct usb_device_descriptor *d) +{ + memset(d, 0, sizeof(*d)); + d->bLength = USB_DT_DEVICE_SIZE; + d->bDescriptorType = USB_DT_DEVICE; + d->bcdUSB = 0x0200; + d->bDeviceClass = 0; + d->bDeviceSubClass = 0; + d->bDeviceProtocol = 0; + d->bMaxPacketSize0 = 64; + d->idVendor = dev->vendor_id; + d->idProduct = dev->product_id; + d->bcdDevice = 0x0100; + d->iManufacturer = 1; + d->iProduct = 2; + d->iSerialNumber = 3; + d->bNumConfigurations = 1; + return (int)sizeof(*d); +} + +/* Minimal string descriptor builder (ASCII -> UTF-16LE). index 0 = langids. */ +static int build_string_descriptor(struct test_virtual_device *dev, __u8 index, + unsigned char *buf, int buflen) +{ + const char *s; + int i, n; + + if (index == 0) { + if (buflen < 4) + return -1; + buf[0] = 4; + buf[1] = USB_DT_STRING; + buf[2] = 0x09; /* 0x0409 English (US) */ + buf[3] = 0x04; + return 4; + } + + switch (index) { + case 1: s = "HIDAPI"; break; + case 2: s = "HIDAPI Test Device"; break; + case 3: s = dev->serial; break; + default: s = ""; break; + } + + n = (int)strlen(s); + if (2 + n * 2 > buflen) + n = (buflen - 2) / 2; + buf[0] = (__u8)(2 + n * 2); + buf[1] = USB_DT_STRING; + for (i = 0; i < n; i++) { + buf[2 + i * 2] = (unsigned char)s[i]; + buf[2 + i * 2 + 1] = 0; + } + return 2 + n * 2; +} + +static void queue_input(struct test_virtual_device *dev, unsigned char command) +{ + pthread_mutex_lock(&dev->lock); + dev->pending = command; + pthread_cond_signal(&dev->cond); + pthread_mutex_unlock(&dev->lock); +} + +/* Handle one EP0 control request. */ +static void handle_control(struct test_virtual_device *dev, + const struct usb_ctrlrequest *ctrl) +{ + unsigned char buf[256]; + int wlen = (int)ctrl->wLength; + int n; + + if ((ctrl->bRequestType & USB_TYPE_MASK) == USB_TYPE_STANDARD) { + switch (ctrl->bRequest) { + case USB_REQ_GET_DESCRIPTOR: { + __u8 type = (__u8)(ctrl->wValue >> 8); + __u8 idx = (__u8)(ctrl->wValue & 0xFF); + if (type == USB_DT_DEVICE) { + struct usb_device_descriptor d; + n = build_device_descriptor(dev, &d); + if (n > wlen) n = wlen; + ep0_write(dev->fd, &d, n); + } else if (type == USB_DT_CONFIG) { + n = build_config_descriptor(buf, (int)sizeof(buf), dev->int_in_addr); + if (n < 0) { ioctl(dev->fd, USB_RAW_IOCTL_EP0_STALL, 0); break; } + if (n > wlen) n = wlen; + ep0_write(dev->fd, buf, n); + } else if (type == USB_DT_STRING) { + n = build_string_descriptor(dev, idx, buf, (int)sizeof(buf)); + if (n < 0) { ioctl(dev->fd, USB_RAW_IOCTL_EP0_STALL, 0); break; } + if (n > wlen) n = wlen; + ep0_write(dev->fd, buf, n); + } else if (type == HID_DT_REPORT) { + n = (int)sizeof(k_report_descriptor); + if (n > wlen) n = wlen; + ep0_write(dev->fd, k_report_descriptor, n); + } else if (type == HID_DT_HID) { + struct hid_class_desc hid; + memset(&hid, 0, sizeof(hid)); + hid.bLength = (__u8)sizeof(hid); + hid.bDescriptorType = HID_DT_HID; + hid.bcdHID = 0x0111; + hid.bNumDescriptors = 1; + hid.bReportType = HID_DT_REPORT; + hid.wReportLength = (__u16)sizeof(k_report_descriptor); + n = (int)sizeof(hid); + if (n > wlen) n = wlen; + ep0_write(dev->fd, &hid, n); + } else { + ioctl(dev->fd, USB_RAW_IOCTL_EP0_STALL, 0); + } + break; + } + case USB_REQ_SET_CONFIGURATION: { + struct usb_endpoint_descriptor ep; + int handle; + memset(&ep, 0, sizeof(ep)); + ep.bLength = USB_DT_ENDPOINT_SIZE; + ep.bDescriptorType = USB_DT_ENDPOINT; + ep.bEndpointAddress = dev->int_in_addr; + ep.bmAttributes = USB_ENDPOINT_XFER_INT; + ep.wMaxPacketSize = TEST_VDEV_REPORT_SIZE; + ep.bInterval = 5; + handle = ioctl(dev->fd, USB_RAW_IOCTL_EP_ENABLE, &ep); + if (handle >= 0) { + dev->int_in_ep = handle; + ioctl(dev->fd, USB_RAW_IOCTL_CONFIGURE, 0); + dev->configured = 1; + } + ep0_ack(dev->fd, ctrl); /* status ACK */ + break; + } + case USB_REQ_SET_INTERFACE: + ep0_ack(dev->fd, ctrl); + break; + case USB_REQ_GET_STATUS: { + unsigned char st[2] = { 0, 0 }; + ep0_write(dev->fd, st, sizeof(st)); + break; + } + default: + ep0_ack(dev->fd, ctrl); + break; + } + return; + } + + if ((ctrl->bRequestType & USB_TYPE_MASK) == USB_TYPE_CLASS) { + if (ctrl->bRequestType & USB_DIR_IN) { + /* GET_REPORT / GET_IDLE / GET_PROTOCOL: benign reply. */ + if (ctrl->bRequest == HID_REQ_GET_REPORT) { + memset(buf, 0, TEST_VDEV_REPORT_SIZE); + n = TEST_VDEV_REPORT_SIZE; + if (n > wlen) n = wlen; + ep0_write(dev->fd, buf, n); + } else { + unsigned char z = 0; + ep0_write(dev->fd, &z, wlen ? 1 : 0); + } + return; + } + + /* Host-to-device class requests. */ + if (ctrl->bRequest == HID_REQ_SET_REPORT) { + unsigned char command = TEST_VDEV_CMD_NONE; + int i, scan; + int len = wlen; + if (len > (int)sizeof(buf)) + len = (int)sizeof(buf); + n = ep0_read(dev->fd, buf, len); /* read data stage + ACK */ + scan = (n > 4) ? 4 : n; + for (i = 0; i < scan; i++) { + if (buf[i] == TEST_VDEV_CMD_EMIT_A || + buf[i] == TEST_VDEV_CMD_EMIT_B) { + command = buf[i]; + break; + } + } + if (command != TEST_VDEV_CMD_NONE) + queue_input(dev, command); + } else { + /* SET_IDLE / SET_PROTOCOL and friends: just ACK. */ + ep0_ack(dev->fd, ctrl); + } + return; + } + + /* Unknown request type. */ + ep0_ack(dev->fd, ctrl); +} + +static void process_eps_info(struct test_virtual_device *dev) +{ + struct usb_raw_eps_info info; + int num, i; + memset(&info, 0, sizeof(info)); + num = ioctl(dev->fd, USB_RAW_IOCTL_EPS_INFO, &info); + if (num < 0) + return; + for (i = 0; i < num && i < USB_RAW_EPS_NUM_MAX; i++) { + if (info.eps[i].caps.type_int && info.eps[i].caps.dir_in) { + __u32 addr = info.eps[i].addr; + if (addr == USB_RAW_EP_ADDR_ANY) + addr = 1; /* pick ep number 1 */ + dev->int_in_addr = (__u8)(0x80 | (addr & 0x0F)); + return; + } + } +} + +static void *ep0_thread_fn(void *arg) +{ + struct test_virtual_device *dev = (struct test_virtual_device *)arg; + struct usb_raw_event *ev; + size_t evsz = sizeof(*ev) + sizeof(struct usb_ctrlrequest); + + ev = (struct usb_raw_event *)calloc(1, evsz); + if (!ev) + return NULL; + + while (!dev->stop) { + int rv; + ev->type = 0; + ev->length = sizeof(struct usb_ctrlrequest); + rv = ioctl(dev->fd, USB_RAW_IOCTL_EVENT_FETCH, ev); + if (rv < 0) { + if (errno == EINTR) + continue; + sleep_ms(10); + continue; + } + + if (ev->type == USB_RAW_EVENT_CONNECT) { + process_eps_info(dev); + } else if (ev->type == USB_RAW_EVENT_CONTROL) { + struct usb_ctrlrequest ctrl; + memcpy(&ctrl, ev->data, sizeof(ctrl)); + handle_control(dev, &ctrl); + } + } + + free(ev); + dev->ep0_exited = 1; + return NULL; +} + +/* Delivers a queued canned input report over the interrupt IN endpoint. The + * EP_WRITE blocks until the host reads it, so this runs on its own thread. */ +static void *int_in_thread_fn(void *arg) +{ + struct test_virtual_device *dev = (struct test_virtual_device *)arg; + + for (;;) { + unsigned char command; + const unsigned char *payload; + + pthread_mutex_lock(&dev->lock); + while (!dev->stop && dev->pending == TEST_VDEV_CMD_NONE) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 100 * 1000000L; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec += 1; + ts.tv_nsec -= 1000000000L; + } + pthread_cond_timedwait(&dev->cond, &dev->lock, &ts); + } + if (dev->stop) { + pthread_mutex_unlock(&dev->lock); + break; + } + command = dev->pending; + dev->pending = TEST_VDEV_CMD_NONE; + pthread_mutex_unlock(&dev->lock); + + if (!dev->configured || dev->int_in_ep < 0) + continue; + + payload = (command == TEST_VDEV_CMD_EMIT_B) ? k_input_b : k_input_a; + /* Best effort: may fail if the host isn't reading; ignore. */ + (void)ep_io_write(dev->fd, USB_RAW_IOCTL_EP_WRITE, dev->int_in_ep, + payload, TEST_VDEV_REPORT_SIZE); + } + return NULL; +} + +int test_virtual_device_create(test_virtual_device **out_dev, + unsigned short vendor_id, + unsigned short product_id, + const char *serial) +{ + struct test_virtual_device *dev; + struct usb_raw_init init; + int rc; + + if (!out_dev) + return TEST_VDEV_ERROR; + *out_dev = NULL; + + dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); + if (!dev) + return TEST_VDEV_ERROR; + + dev->fd = -1; + dev->int_in_ep = -1; + dev->int_in_addr = 0x81; + dev->pending = TEST_VDEV_CMD_NONE; + dev->vendor_id = vendor_id; + dev->product_id = product_id; + snprintf(dev->serial, sizeof(dev->serial), "%s", serial ? serial : ""); + pthread_mutex_init(&dev->lock, NULL); + pthread_cond_init(&dev->cond, NULL); + + dev->fd = open("/dev/raw-gadget", O_RDWR); + if (dev->fd < 0) { + int e = errno; + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + if (e == ENOENT || e == EACCES || e == EPERM || e == ENODEV) + return TEST_VDEV_UNAVAILABLE; + return TEST_VDEV_ERROR; + } + + memset(&init, 0, sizeof(init)); + /* dummy_hcd registers a UDC named "dummy_udc.0". */ + snprintf((char *)init.driver_name, sizeof(init.driver_name), "dummy_udc"); + snprintf((char *)init.device_name, sizeof(init.device_name), "dummy_udc.0"); + init.speed = USB_SPEED_HIGH; + if (ioctl(dev->fd, USB_RAW_IOCTL_INIT, &init) < 0 || + ioctl(dev->fd, USB_RAW_IOCTL_RUN, 0) < 0) { + /* No dummy_hcd UDC present -> nothing to emulate on; skip. */ + close(dev->fd); + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return TEST_VDEV_UNAVAILABLE; + } + + rg_install_signal(); + + rc = pthread_create(&dev->ep0_thread, NULL, ep0_thread_fn, dev); + if (rc != 0) + goto fail_threads; + dev->ep0_started = 1; + + rc = pthread_create(&dev->int_in_thread, NULL, int_in_thread_fn, dev); + if (rc != 0) + goto fail_threads; + dev->int_in_started = 1; + + *out_dev = dev; + return TEST_VDEV_OK; + +fail_threads: + dev->stop = 1; + pthread_mutex_lock(&dev->lock); + pthread_cond_broadcast(&dev->cond); + pthread_mutex_unlock(&dev->lock); + if (dev->ep0_started) { + int spins = 0; + while (!dev->ep0_exited && spins++ < 500) { + pthread_kill(dev->ep0_thread, SIGUSR1); + sleep_ms(10); + } + pthread_join(dev->ep0_thread, NULL); + } + close(dev->fd); + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); + return TEST_VDEV_ERROR; +} + +hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms) +{ + wchar_t wserial[64]; + int waited = 0; + size_t i; + + if (!dev) + return NULL; + + for (i = 0; i + 1 < (sizeof(wserial) / sizeof(wserial[0])) && dev->serial[i]; i++) + wserial[i] = (wchar_t)(unsigned char)dev->serial[i]; + wserial[i] = L'\0'; + + for (;;) { + struct hid_device_info *infos = hid_enumerate(dev->vendor_id, dev->product_id); + struct hid_device_info *cur; + struct hid_device_info *first = NULL; + struct hid_device_info *match = NULL; + hid_device *h = NULL; + + for (cur = infos; cur; cur = cur->next) { + if (!first) + first = cur; + if (cur->serial_number && wcscmp(cur->serial_number, wserial) == 0) { + match = cur; + break; + } + } + if (match || first) + h = hid_open_path((match ? match : first)->path); + hid_free_enumeration(infos); + if (h) + return h; + + if (waited >= timeout_ms) + return NULL; + sleep_ms(50); + waited += 50; + } +} + +int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, + unsigned char command) +{ + unsigned char feature[1 + TEST_VDEV_REPORT_SIZE]; + (void)dev; + memset(feature, 0, sizeof(feature)); + feature[0] = 0x00; /* Report ID (the device has no numbered reports) */ + feature[1] = command; /* scenario command, first byte of the payload */ + return hid_send_feature_report(handle, feature, sizeof(feature)); +} + +void test_virtual_device_destroy(test_virtual_device *dev) +{ + if (!dev) + return; + + dev->stop = 1; + pthread_mutex_lock(&dev->lock); + pthread_cond_broadcast(&dev->cond); + pthread_mutex_unlock(&dev->lock); + + /* The ep0 thread is parked in the blocking EVENT_FETCH ioctl (and the + int_in thread may be in EP_WRITE); interrupt them with SIGUSR1 until the + ep0 thread reports it has left its loop, so the joins below don't hang. */ + { + int spins = 0; + while (dev->ep0_started && !dev->ep0_exited && spins++ < 500) { + pthread_kill(dev->ep0_thread, SIGUSR1); + if (dev->int_in_started) + pthread_kill(dev->int_in_thread, SIGUSR1); + sleep_ms(10); + } + } + + if (dev->int_in_started) { + pthread_join(dev->int_in_thread, NULL); + dev->int_in_started = 0; + } + if (dev->ep0_started) { + pthread_join(dev->ep0_thread, NULL); + dev->ep0_started = 0; + } + + if (dev->fd >= 0) { + close(dev->fd); + dev->fd = -1; + } + + pthread_cond_destroy(&dev->cond); + pthread_mutex_destroy(&dev->lock); + free(dev); +} diff --git a/src/tests/test_virtual_device_uhid.c b/src/tests/test_virtual_device_uhid.c new file mode 100644 index 00000000..a86fd1da --- /dev/null +++ b/src/tests/test_virtual_device_uhid.c @@ -0,0 +1,358 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Linux implementation of the virtual HID device test interface, + backed by the kernel's userspace-HID interface (/dev/uhid). + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +/* + * Creating a uhid device makes the kernel expose a real /dev/hidrawN node that + * the HIDAPI hidraw backend can enumerate and open. The pre-recorded scenarios + * (see test_virtual_device.h) are played back here: the uhid event pump watches + * for a Feature SET_REPORT whose first byte is a TEST_VDEV_CMD_* command and + * replays the matching canned input report. Requires the 'uhid' module and + * (typically) root to open /dev/uhid; otherwise create() returns + * TEST_VDEV_UNAVAILABLE so the test is skipped rather than failed. + */ + +#include "test_virtual_device.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +struct test_virtual_device { + int fd; /* /dev/uhid file descriptor */ + pthread_t pump_thread; + int pump_started; + volatile int stop; /* tells the event pump to exit */ + + unsigned short vendor_id; + unsigned short product_id; + char serial[64]; /* uhid 'uniq' == HIDAPI serial_number */ + + pthread_mutex_t write_lock; /* serializes writes to the uhid fd */ +}; + +/* A generic vendor-defined descriptor: one 8-byte Input, Output and Feature + * report, no Report ID. With no Report ID, hid_read() returns the replayed + * input bytes verbatim, which keeps the tests easy to reason about. */ +static const unsigned char k_report_descriptor[] = { + 0x06, 0x00, 0xFF, /* Usage Page (Vendor Defined 0xFF00) */ + 0x09, 0x01, /* Usage (0x01) */ + 0xA1, 0x01, /* Collection (Application) */ + 0x09, 0x01, /* Usage (0x01) */ + 0x15, 0x00, /* Logical Minimum (0) */ + 0x26, 0xFF, 0x00, /* Logical Maximum (255) */ + 0x75, 0x08, /* Report Size (8) */ + 0x95, 0x08, /* Report Count (8) */ + 0x81, 0x02, /* Input (Data,Var,Abs) */ + 0x09, 0x01, /* Usage (0x01) */ + 0x91, 0x02, /* Output (Data,Var,Abs) */ + 0x09, 0x01, /* Usage (0x01) */ + 0xB1, 0x02, /* Feature (Data,Var,Abs) */ + 0xC0 /* End Collection */ +}; + +static const unsigned char k_input_a[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_A; +static const unsigned char k_input_b[TEST_VDEV_REPORT_SIZE] = TEST_VDEV_INPUT_B; + +/* write() is marked warn_unused_result by glibc; consume the result for the + * best-effort writes (replies, teardown). */ +static void write_event_best_effort(int fd, const struct uhid_event *ev) +{ + ssize_t r = write(fd, ev, sizeof(*ev)); + (void)r; +} + +static void sleep_ms(int ms) +{ + struct timespec ts; + ts.tv_sec = ms / 1000; + ts.tv_nsec = (long)(ms % 1000) * 1000000L; + nanosleep(&ts, NULL); +} + +/* Send one input report (size TEST_VDEV_REPORT_SIZE) to the host. */ +static void emit_input(struct test_virtual_device *dev, const unsigned char *payload) +{ + struct uhid_event ev; + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_INPUT2; + ev.u.input2.size = TEST_VDEV_REPORT_SIZE; + memcpy(ev.u.input2.data, payload, TEST_VDEV_REPORT_SIZE); + + pthread_mutex_lock(&dev->write_lock); + write_event_best_effort(dev->fd, &ev); + pthread_mutex_unlock(&dev->write_lock); +} + +/* A Feature SET_REPORT carries a scenario command. Depending on whether the + * report is numbered, the payload the kernel hands to uhid may be prefixed by + * a report-number byte, so locate the command by scanning the first few bytes + * for a recognised TEST_VDEV_CMD_* value (the report-number byte is 0 for our + * unnumbered device, and the commands are non-zero, so this is unambiguous). */ +static void handle_set_report(struct test_virtual_device *dev, const struct uhid_event *in) +{ + struct uhid_event out; + unsigned char command = TEST_VDEV_CMD_NONE; + uint16_t scan = in->u.set_report.size; + uint16_t i; + if (scan > 4) + scan = 4; + for (i = 0; i < scan; i++) { + unsigned char b = in->u.set_report.data[i]; + if (b == TEST_VDEV_CMD_EMIT_A || b == TEST_VDEV_CMD_EMIT_B) { + command = b; + break; + } + } + + memset(&out, 0, sizeof(out)); + out.type = UHID_SET_REPORT_REPLY; + out.u.set_report_reply.id = in->u.set_report.id; + out.u.set_report_reply.err = 0; + pthread_mutex_lock(&dev->write_lock); + write_event_best_effort(dev->fd, &out); + pthread_mutex_unlock(&dev->write_lock); + + switch (command) { + case TEST_VDEV_CMD_EMIT_A: + emit_input(dev, k_input_a); + break; + case TEST_VDEV_CMD_EMIT_B: + emit_input(dev, k_input_b); + break; + default: + break; + } +} + +/* Answer feature GET_REPORT requests benignly (empty payload). */ +static void handle_get_report(struct test_virtual_device *dev, const struct uhid_event *in) +{ + struct uhid_event out; + memset(&out, 0, sizeof(out)); + out.type = UHID_GET_REPORT_REPLY; + out.u.get_report_reply.id = in->u.get_report.id; + out.u.get_report_reply.err = 0; + out.u.get_report_reply.size = 0; + pthread_mutex_lock(&dev->write_lock); + write_event_best_effort(dev->fd, &out); + pthread_mutex_unlock(&dev->write_lock); +} + +static void *pump_thread_fn(void *arg) +{ + struct test_virtual_device *dev = (struct test_virtual_device *)arg; + + while (!dev->stop) { + struct pollfd pfd; + int pret; + ssize_t rret; + struct uhid_event ev; + + pfd.fd = dev->fd; + pfd.events = POLLIN; + pfd.revents = 0; + + pret = poll(&pfd, 1, 100); + if (pret <= 0) + continue; /* timeout (re-check stop) or EINTR */ + + if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) + break; + + memset(&ev, 0, sizeof(ev)); + rret = read(dev->fd, &ev, sizeof(ev)); + if (rret < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + break; + } + + switch (ev.type) { + case UHID_SET_REPORT: + handle_set_report(dev, &ev); + break; + case UHID_GET_REPORT: + handle_get_report(dev, &ev); + break; + default: + /* UHID_START / UHID_OPEN / UHID_CLOSE / UHID_STOP / UHID_OUTPUT */ + break; + } + } + + return NULL; +} + +int test_virtual_device_create(test_virtual_device **out_dev, + unsigned short vendor_id, + unsigned short product_id, + const char *serial) +{ + struct test_virtual_device *dev; + struct uhid_event ev; + int rc; + + if (!out_dev) + return TEST_VDEV_ERROR; + *out_dev = NULL; + + dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); + if (!dev) + return TEST_VDEV_ERROR; + + dev->fd = -1; + dev->vendor_id = vendor_id; + dev->product_id = product_id; + snprintf(dev->serial, sizeof(dev->serial), "%s", serial ? serial : ""); + pthread_mutex_init(&dev->write_lock, NULL); + + dev->fd = open("/dev/uhid", O_RDWR | O_CLOEXEC); + if (dev->fd < 0) { + int e = errno; + pthread_mutex_destroy(&dev->write_lock); + free(dev); + if (e == ENOENT || e == EACCES || e == EPERM || e == ENODEV) + return TEST_VDEV_UNAVAILABLE; + return TEST_VDEV_ERROR; + } + + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_CREATE2; + snprintf((char *)ev.u.create2.name, sizeof(ev.u.create2.name), "HIDAPI Test Device"); + snprintf((char *)ev.u.create2.uniq, sizeof(ev.u.create2.uniq), "%s", dev->serial); + memcpy(ev.u.create2.rd_data, k_report_descriptor, sizeof(k_report_descriptor)); + ev.u.create2.rd_size = (uint16_t)sizeof(k_report_descriptor); + ev.u.create2.bus = 0x03; /* BUS_USB */ + ev.u.create2.vendor = vendor_id; + ev.u.create2.product = product_id; + ev.u.create2.version = 0; + ev.u.create2.country = 0; + + if (write(dev->fd, &ev, sizeof(ev)) < 0) { + int e = errno; + close(dev->fd); + pthread_mutex_destroy(&dev->write_lock); + free(dev); + if (e == EACCES || e == EPERM) + return TEST_VDEV_UNAVAILABLE; + return TEST_VDEV_ERROR; + } + + rc = pthread_create(&dev->pump_thread, NULL, pump_thread_fn, dev); + if (rc != 0) { + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_DESTROY; + write_event_best_effort(dev->fd, &ev); + close(dev->fd); + pthread_mutex_destroy(&dev->write_lock); + free(dev); + return TEST_VDEV_ERROR; + } + dev->pump_started = 1; + + *out_dev = dev; + return TEST_VDEV_OK; +} + +hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms) +{ + wchar_t wserial[64]; + int waited = 0; + size_t i; + + if (!dev) + return NULL; + + for (i = 0; i + 1 < (sizeof(wserial) / sizeof(wserial[0])) && dev->serial[i]; i++) + wserial[i] = (wchar_t)(unsigned char)dev->serial[i]; + wserial[i] = L'\0'; + + /* The hidraw node and its udev attributes appear asynchronously after + UHID_CREATE2; poll enumeration until the device shows up. Match by the + (test-unique) VID/PID, preferring the entry whose serial matches. */ + for (;;) { + struct hid_device_info *infos = hid_enumerate(dev->vendor_id, dev->product_id); + struct hid_device_info *cur; + struct hid_device_info *first = NULL; + struct hid_device_info *match = NULL; + hid_device *h = NULL; + + for (cur = infos; cur; cur = cur->next) { + if (!first) + first = cur; + if (cur->serial_number && wcscmp(cur->serial_number, wserial) == 0) { + match = cur; + break; + } + } + if (match || first) + h = hid_open_path((match ? match : first)->path); + hid_free_enumeration(infos); + if (h) + return h; + + if (waited >= timeout_ms) + return NULL; + sleep_ms(50); + waited += 50; + } +} + +int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, + unsigned char command) +{ + unsigned char feature[1 + TEST_VDEV_REPORT_SIZE]; + (void)dev; + memset(feature, 0, sizeof(feature)); + feature[0] = 0x00; /* Report ID (the device has no numbered reports) */ + feature[1] = command; /* scenario command, first byte of the payload */ + return hid_send_feature_report(handle, feature, sizeof(feature)); +} + +void test_virtual_device_destroy(test_virtual_device *dev) +{ + struct uhid_event ev; + + if (!dev) + return; + + if (dev->pump_started) { + dev->stop = 1; + pthread_join(dev->pump_thread, NULL); + dev->pump_started = 0; + } + + if (dev->fd >= 0) { + memset(&ev, 0, sizeof(ev)); + ev.type = UHID_DESTROY; + write_event_best_effort(dev->fd, &ev); + close(dev->fd); + dev->fd = -1; + } + + pthread_mutex_destroy(&dev->write_lock); + free(dev); +} diff --git a/src/tests/test_virtual_device_win.c b/src/tests/test_virtual_device_win.c new file mode 100644 index 00000000..6ad0961e --- /dev/null +++ b/src/tests/test_virtual_device_win.c @@ -0,0 +1,146 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + libusb/hidapi Team + + Copyright 2026. + + Windows implementation of the virtual HID device test interface. + + The contents of this file may be used by anyone for any + reason without any conditions and may be used as a + starting point for your own applications which use HIDAPI. +********************************************************/ + +/* + * On Windows there is no userspace facility to create a HID device on the fly + * (unlike Linux /dev/uhid). A small UMDF virtual HID driver + * (src/tests/windows/driver, a modified vhidmini2) is built, signed and + * installed out-of-band by the CI job before the test runs, and removed + * afterwards. That driver implements the same pre-recorded scenario protocol + * as the Linux uhid provider (see test_virtual_device.h). + * + * create() just records the ids; presence is confirmed by open_hidapi(), which + * also caches the device's feature-report length so that trigger() can send a + * feature report of exactly the size Windows requires. + */ + +#include "test_virtual_device.h" + +#include +#include +#include +#include +#include +#include + +struct test_virtual_device { + unsigned short vendor_id; + unsigned short product_id; + char serial[64]; + ULONG feature_len; /* FeatureReportByteLength of the opened device */ +}; + +int test_virtual_device_create(test_virtual_device **out_dev, + unsigned short vendor_id, + unsigned short product_id, + const char *serial) +{ + struct test_virtual_device *dev; + + if (!out_dev) + return TEST_VDEV_ERROR; + *out_dev = NULL; + + dev = (struct test_virtual_device *)calloc(1, sizeof(*dev)); + if (!dev) + return TEST_VDEV_ERROR; + + dev->vendor_id = vendor_id; + dev->product_id = product_id; + if (serial) + strncpy_s(dev->serial, sizeof(dev->serial), serial, _TRUNCATE); + + /* The device (if any) is installed by the harness; presence is verified + by open_hidapi(). */ + *out_dev = dev; + return TEST_VDEV_OK; +} + +/* Query the device's HID caps (report byte lengths) directly from Windows. */ +static ULONG query_feature_len(const char *path) +{ + HANDLE h; + PHIDP_PREPARSED_DATA pp = NULL; + ULONG feat = 0; + + h = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, + OPEN_EXISTING, 0, NULL); + if (h == INVALID_HANDLE_VALUE) + return 0; + + if (HidD_GetPreparsedData(h, &pp)) { + HIDP_CAPS caps; + if (HidP_GetCaps(pp, &caps) == HIDP_STATUS_SUCCESS) { + printf(" device caps: input=%u output=%u feature=%u\n", + (unsigned)caps.InputReportByteLength, + (unsigned)caps.OutputReportByteLength, + (unsigned)caps.FeatureReportByteLength); + feat = caps.FeatureReportByteLength; + } + HidD_FreePreparsedData(pp); + } + CloseHandle(h); + return feat; +} + +hid_device *test_virtual_device_open_hidapi(test_virtual_device *dev, int timeout_ms) +{ + int waited = 0; + + if (!dev) + return NULL; + + for (;;) { + struct hid_device_info *infos = hid_enumerate(dev->vendor_id, dev->product_id); + hid_device *h = NULL; + + if (infos) { + if (dev->feature_len == 0) + dev->feature_len = query_feature_len(infos->path); + h = hid_open_path(infos->path); + } + hid_free_enumeration(infos); + if (h) + return h; + + if (waited >= timeout_ms) + return NULL; + Sleep(50); + waited += 50; + } +} + +int test_virtual_device_trigger(test_virtual_device *dev, hid_device *handle, + unsigned char command) +{ + unsigned char feature[256]; + size_t len = (1 + TEST_VDEV_REPORT_SIZE); + + /* Windows requires the feature buffer to be exactly FeatureReportByteLength. */ + if (dev && dev->feature_len > 0 && dev->feature_len <= sizeof(feature)) + len = dev->feature_len; + + memset(feature, 0, sizeof(feature)); + feature[0] = 0x00; /* Report ID (the device has no numbered reports) */ + feature[1] = command; /* scenario command, first byte of the payload */ + return hid_send_feature_report(handle, feature, len); +} + +void test_virtual_device_destroy(test_virtual_device *dev) +{ + /* The harness uninstalls the driver/device after the test. */ + free(dev); +} diff --git a/src/tests/windows/driver/LICENSE.txt b/src/tests/windows/driver/LICENSE.txt new file mode 100644 index 00000000..6ef3ee38 --- /dev/null +++ b/src/tests/windows/driver/LICENSE.txt @@ -0,0 +1,23 @@ +The Microsoft Public License (MS-PL) +Copyright (c) 2015 Microsoft + +This license governs use of the accompanying software. If you use the software, you + accept this license. If you do not accept the license, do not use the software. + +1. Definitions + The terms "reproduce," "reproduction," "derivative works," and "distribution" have the + same meaning here as under U.S. copyright law. + A "contribution" is the original software, or any additions or changes to the software. + A "contributor" is any person that distributes its contribution under this license. + "Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights + (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. + (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations + (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. + (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. + (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. + (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. + (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. diff --git a/src/tests/windows/driver/README.md b/src/tests/windows/driver/README.md new file mode 100644 index 00000000..07a7651b --- /dev/null +++ b/src/tests/windows/driver/README.md @@ -0,0 +1,41 @@ +# Vendored vhidmini2 UMDF2 driver (HIDAPI test fixture) + +This directory contains a small **virtual HID minidriver** used only by the +HIDAPI virtual-device tests on Windows (the `winapi` backend's +`DeviceIO_winapi` test). It is **not** part of the HIDAPI library: it is a +standalone UMDF 2 driver that the `win-vhid-test` CI job builds, self-signs and +installs out-of-band, runs the test against, then removes. + +## Provenance + +These files are **derived from the `hid/vhidmini2` sample** in Microsoft's +[`microsoft/Windows-driver-samples`](https://github.com/microsoft/Windows-driver-samples) +repository (the UMDF 2 "VhidminiUm" variant). + +## License + +The upstream sample repository is licensed under the **Microsoft Public License +(MS-PL)**. A complete, verbatim copy of that license is included here as +[`LICENSE.txt`](./LICENSE.txt), and the original Microsoft copyright notices are +retained in each file's header. (Those per-file headers carry Microsoft's older +sample boilerplate — "All Rights Reserved" / "THIS CODE … IS PROVIDED 'AS IS'" — +but the governing license for the sample is the repository-level MS-PL.) + +**The files in this directory remain under the MS-PL**, which is *separate* from +HIDAPI's own licensing. HIDAPI itself is offered under your choice of the GNU +GPL v3, a BSD-style license, or the original HIDAPI license (see the +`LICENSE*.txt` files at the root of this repository); that choice does **not** +apply to the MS-PL files here. + +MS-PL is not GPL-compatible, but this driver is a **separate program**, not a +derivative or a linked part of the HIDAPI library: it is built independently and +communicates with HIDAPI only at runtime, through the Windows HID stack. Its +presence in the source tree is therefore mere aggregation, not a combined work. + +## What was modified + +| File | Status | +|------|--------| +| `vhidmini.c` | **Modified** — default report descriptor matches the Linux uhid test device byte-for-byte; implements the HIDAPI pre-recorded "scenario" protocol (a Feature `SET_REPORT` command makes the device replay a canned input report; see `../../test_virtual_device.h`). | +| `vhidmini.h` | **Modified** — supporting declarations for the scenario protocol. | +| `common.h`, `util.c`, `vhidmini.rc`, `VhidminiUm.inx`, `VhidminiUm.vcxproj` | Used essentially as-is (no HIDAPI-specific changes beyond what's needed to build the standalone `VhidminiUm.dll`). | diff --git a/src/tests/windows/driver/VhidminiUm.inx b/src/tests/windows/driver/VhidminiUm.inx new file mode 100644 index 00000000..aa617a3f Binary files /dev/null and b/src/tests/windows/driver/VhidminiUm.inx differ diff --git a/src/tests/windows/driver/VhidminiUm.vcxproj b/src/tests/windows/driver/VhidminiUm.vcxproj new file mode 100644 index 00000000..0a7e3971 --- /dev/null +++ b/src/tests/windows/driver/VhidminiUm.vcxproj @@ -0,0 +1,213 @@ + + + + + Debug + ARM64 + + + Release + ARM64 + + + Debug + x64 + + + Release + x64 + + + + {EEECA421-064D-4E2E-8832-B0F43A9B394C} + $(MSBuildProjectName) + 2 + Debug + x64 + {1F3C5D0E-2636-4FC0-946E-F3B967B2715D} + + + + Windows10 + False + Windows Driver + UMDF + WindowsUserModeDriver10.0 + DynamicLibrary + + + Windows10 + False + Windows Driver + UMDF + WindowsUserModeDriver10.0 + DynamicLibrary + + + Windows10 + True + Windows Driver + UMDF + WindowsUserModeDriver10.0 + DynamicLibrary + + + Windows10 + True + Windows Driver + UMDF + WindowsUserModeDriver10.0 + DynamicLibrary + + + + $(IntDir) + + + + + + + + + + + + + + + + VhidminiUm + + + VhidminiUm + + + VhidminiUm + + + VhidminiUm + + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(AdditionalDependencies);mincore.lib + + + sha256 + + + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(AdditionalDependencies);mincore.lib + + + sha256 + + + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(AdditionalDependencies);mincore.lib + + + sha256 + + + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(PreprocessorDefinitions);_UNICODE;UNICODE + %(AdditionalIncludeDirectories);$(DDK_INC_PATH);$(DDK_INC_PATH)\wdm;..\..\inc;.. + + + %(AdditionalDependencies);mincore.lib + + + sha256 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/tests/windows/driver/common.h b/src/tests/windows/driver/common.h new file mode 100644 index 00000000..9b14cc6d --- /dev/null +++ b/src/tests/windows/driver/common.h @@ -0,0 +1,119 @@ +/*++ + +Copyright (c) Microsoft Corporation. All rights reserved. + + THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY + KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR + PURPOSE. + +Module Name: + + common.h + +Environment: + + User mode + +--*/ + +#ifndef __VHIDMINI_COMMON_H__ +#define __VHIDMINI_COMMON_H__ + +// +// Custom control codes are defined here. They are to be used for sideband +// communication with the hid minidriver. These control codes are sent to +// the hid minidriver using Hid_SetFeature() API to a custom collection +// defined especially to handle such requests. +// +#define HIDMINI_CONTROL_CODE_SET_ATTRIBUTES 0x00 +#define HIDMINI_CONTROL_CODE_DUMMY1 0x01 +#define HIDMINI_CONTROL_CODE_DUMMY2 0x02 + +// +// This is the report id of the collection to which the control codes are sent +// +#define CONTROL_COLLECTION_REPORT_ID 0x01 +#define TEST_COLLECTION_REPORT_ID 0x02 + +#define MAXIMUM_STRING_LENGTH (126 * sizeof(WCHAR)) +#define VHIDMINI_MANUFACTURER_STRING L"UMDF Virtual hidmini device Manufacturer string" +#define VHIDMINI_PRODUCT_STRING L"UMDF Virtual hidmini device Product string" +#define VHIDMINI_SERIAL_NUMBER_STRING L"UMDF Virtual hidmini device Serial Number string" +#define VHIDMINI_DEVICE_STRING L"UMDF Virtual hidmini device" +#define VHIDMINI_DEVICE_STRING_INDEX 5 +#include + +typedef struct _MY_DEVICE_ATTRIBUTES { + + USHORT VendorID; + USHORT ProductID; + USHORT VersionNumber; + +} MY_DEVICE_ATTRIBUTES, *PMY_DEVICE_ATTRIBUTES; + +typedef struct _HIDMINI_CONTROL_INFO { + + // + //report ID of the collection to which the control request is sent + // + UCHAR ReportId; + + // + // One byte control code (user-defined) for communication with hid + // mini driver + // + UCHAR ControlCode; + + // + // This union contains input data for the control request. + // + union { + MY_DEVICE_ATTRIBUTES Attributes; + struct { + ULONG Dummy1; + ULONG Dummy2; + } Dummy; + } u; + +} HIDMINI_CONTROL_INFO, * PHIDMINI_CONTROL_INFO; + +// +// input from device to system +// +typedef struct _HIDMINI_INPUT_REPORT { + + UCHAR ReportId; + + UCHAR Data; + +} HIDMINI_INPUT_REPORT, *PHIDMINI_INPUT_REPORT; + +// +// output to device from system +// +typedef struct _HIDMINI_OUTPUT_REPORT { + + UCHAR ReportId; + + UCHAR Data; + + USHORT Pad1; + + ULONG Pad2; + +} HIDMINI_OUTPUT_REPORT, *PHIDMINI_OUTPUT_REPORT; + +#include + +// +// SetFeature request requires that the feature report buffer size be exactly +// same as the size of report described in the hid report descriptor ( +// excluding the report ID). Since HIDMINI_CONTROL_INFO includes report ID, +// we subtract one from the size. +// +#define FEATURE_REPORT_SIZE_CB ((USHORT)(sizeof(HIDMINI_CONTROL_INFO) - 1)) +#define INPUT_REPORT_SIZE_CB ((USHORT)(sizeof(HIDMINI_INPUT_REPORT) - 1)) +#define OUTPUT_REPORT_SIZE_CB ((USHORT)(sizeof(HIDMINI_OUTPUT_REPORT) - 1)) + +#endif //__VHIDMINI_COMMON_H__ diff --git a/src/tests/windows/driver/util.c b/src/tests/windows/driver/util.c new file mode 100644 index 00000000..6d0eb419 --- /dev/null +++ b/src/tests/windows/driver/util.c @@ -0,0 +1,149 @@ +/*++ + +Copyright (C) Microsoft Corporation, All Rights Reserved. + +Module Name: + + util.cpp + +Abstract: + + This module contains the implementation of the driver + +Environment: + + Windows Driver Framework (WDF) + +--*/ + +#include "vhidmini.h" + +// +// HID minidriver IOCTL uses HID_XFER_PACKET which contains an embedded pointer. +// +// typedef struct _HID_XFER_PACKET { +// PUCHAR reportBuffer; +// ULONG reportBufferLen; +// UCHAR reportId; +// } HID_XFER_PACKET, *PHID_XFER_PACKET; +// +// UMDF cannot handle embedded pointers when marshalling buffers between processes. +// Therefore a special driver mshidumdf.sys is introduced to convert such IRPs to +// new IRPs (with new IOCTL name like IOCTL_UMDF_HID_Xxxx) where: +// +// reportBuffer - passed as one buffer inside the IRP +// reportId - passed as a second buffer inside the IRP +// +// The new IRP is then passed to UMDF host and driver for further processing. +// + +NTSTATUS +RequestGetHidXferPacket_ToReadFromDevice( + _In_ WDFREQUEST Request, + _Out_ HID_XFER_PACKET *Packet + ) +{ + // + // Driver need to write to the output buffer (so that App can read from it) + // + // Report Buffer: Output Buffer + // Report Id : Input Buffer + // + + NTSTATUS status; + WDFMEMORY inputMemory; + WDFMEMORY outputMemory; + size_t inputBufferLength; + size_t outputBufferLength; + PVOID inputBuffer; + PVOID outputBuffer; + + // + // Get report Id from input buffer + // + status = WdfRequestRetrieveInputMemory(Request, &inputMemory); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfRequestRetrieveInputMemory failed 0x%x\n",status)); + return status; + } + inputBuffer = WdfMemoryGetBuffer(inputMemory, &inputBufferLength); + + if (inputBufferLength < sizeof(UCHAR)) { + status = STATUS_INVALID_BUFFER_SIZE; + KdPrint(("WdfRequestRetrieveInputMemory: invalid input buffer. size %d, expect %d\n", + (int)inputBufferLength, (int)sizeof(UCHAR))); + return status; + } + + Packet->reportId = *(PUCHAR)inputBuffer; + + // + // Get report buffer from output buffer + // + status = WdfRequestRetrieveOutputMemory(Request, &outputMemory); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfRequestRetrieveOutputMemory failed 0x%x\n",status)); + return status; + } + + outputBuffer = WdfMemoryGetBuffer(outputMemory, &outputBufferLength); + + Packet->reportBuffer = (PUCHAR) outputBuffer; + Packet->reportBufferLen = (ULONG) outputBufferLength; + + return status; +} + +NTSTATUS +RequestGetHidXferPacket_ToWriteToDevice( + _In_ WDFREQUEST Request, + _Out_ HID_XFER_PACKET *Packet + ) +{ + // + // Driver need to read from the input buffer (which was written by App) + // + // Report Buffer: Input Buffer + // Report Id : Output Buffer Length + // + // Note that the report id is not stored inside the output buffer, as the + // driver has no read-access right to the output buffer, and trying to read + // from the buffer will cause an access violation error. + // + // The workaround is to store the report id in the OutputBufferLength field, + // to which the driver does have read-access right. + // + + NTSTATUS status; + WDFMEMORY inputMemory; + WDFMEMORY outputMemory; + size_t inputBufferLength; + size_t outputBufferLength; + PVOID inputBuffer; + + // + // Get report Id from output buffer length + // + status = WdfRequestRetrieveOutputMemory(Request, &outputMemory); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfRequestRetrieveOutputMemory failed 0x%x\n",status)); + return status; + } + WdfMemoryGetBuffer(outputMemory, &outputBufferLength); + Packet->reportId = (UCHAR) outputBufferLength; + + // + // Get report buffer from input buffer + // + status = WdfRequestRetrieveInputMemory(Request, &inputMemory); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfRequestRetrieveInputMemory failed 0x%x\n",status)); + return status; + } + inputBuffer = WdfMemoryGetBuffer(inputMemory, &inputBufferLength); + + Packet->reportBuffer = (PUCHAR) inputBuffer; + Packet->reportBufferLen = (ULONG) inputBufferLength; + + return status; +} diff --git a/src/tests/windows/driver/vhidmini.c b/src/tests/windows/driver/vhidmini.c new file mode 100644 index 00000000..6c92cc5e --- /dev/null +++ b/src/tests/windows/driver/vhidmini.c @@ -0,0 +1,1495 @@ +/*++ + +Copyright (C) Microsoft Corporation, All Rights Reserved. + +Module Name: + + vhidmini.cpp + +Abstract: + + This module contains the implementation of the driver + +Environment: + + Windows Driver Framework (WDF) + +--*/ + +/* + * Modified by the libusb/hidapi team for the HIDAPI virtual-device tests: + * the default report descriptor matches the Linux uhid test device and the + * driver implements the pre-recorded "scenario" protocol (a Feature SET_REPORT + * whose first payload byte is a command makes the device replay a canned input + * report; see src/tests/test_virtual_device.h). Derived from the vhidmini2 + * sample in microsoft/Windows-driver-samples, which is licensed under the + * Microsoft Public License (MS-PL); see README.md and LICENSE.txt in this + * directory. + */ + +#include "vhidmini.h" + +// +// This is the default report descriptor for the virtual Hid device returned +// by the mini driver in response to IOCTL_HID_GET_REPORT_DESCRIPTOR. +// +// Unnumbered vendor-defined device: one 8-byte Input, Output and Feature +// report, no Report ID. This matches the Linux uhid test device byte-for-byte +// (see src/tests/test_virtual_device_uhid.c) so the same test drives both. +HID_REPORT_DESCRIPTOR G_DefaultReportDescriptor[] = { + 0x06, 0x00, 0xFF, // USAGE_PAGE (Vendor Defined 0xFF00) + 0x09, 0x01, // USAGE (Vendor Usage 0x01) + 0xA1, 0x01, // COLLECTION (Application) + 0x09, 0x01, // USAGE (Vendor Usage 0x01) + 0x15, 0x00, // LOGICAL_MINIMUM(0) + 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM(255) + 0x75, 0x08, // REPORT_SIZE (8) + 0x95, TEST_VDEV_REPORT_SIZE, // REPORT_COUNT (8) + 0x81, 0x02, // INPUT (Data,Var,Abs) + 0x09, 0x01, // USAGE (Vendor Usage 0x01) + 0x91, 0x02, // OUTPUT (Data,Var,Abs) + 0x09, 0x01, // USAGE (Vendor Usage 0x01) + 0xB1, 0x02, // FEATURE (Data,Var,Abs) + 0xC0, // END_COLLECTION +}; + +// +// This is the default HID descriptor returned by the mini driver +// in response to IOCTL_HID_GET_DEVICE_DESCRIPTOR. The size +// of report descriptor is currently the size of G_DefaultReportDescriptor. +// + +HID_DESCRIPTOR G_DefaultHidDescriptor = { + 0x09, // length of HID descriptor + 0x21, // descriptor type == HID 0x21 + 0x0100, // hid spec release + 0x00, // country code == Not Specified + 0x01, // number of HID class descriptors + { //DescriptorList[0] + 0x22, //report descriptor type 0x22 + sizeof(G_DefaultReportDescriptor) //total length of report descriptor + } +}; + +NTSTATUS +DriverEntry( + _In_ PDRIVER_OBJECT DriverObject, + _In_ PUNICODE_STRING RegistryPath + ) +/*++ + +Routine Description: + DriverEntry initializes the driver and is the first routine called by the + system after the driver is loaded. DriverEntry specifies the other entry + points in the function driver, such as EvtDevice and DriverUnload. + +Parameters Description: + + DriverObject - represents the instance of the function driver that is loaded + into memory. DriverEntry must initialize members of DriverObject before it + returns to the caller. DriverObject is allocated by the system before the + driver is loaded, and it is released by the system after the system unloads + the function driver from memory. + + RegistryPath - represents the driver specific path in the Registry. + The function driver can use the path to store driver related data between + reboots. The path does not store hardware instance specific data. + +Return Value: + + STATUS_SUCCESS, or another status value for which NT_SUCCESS(status) equals + TRUE if successful, + + STATUS_UNSUCCESSFUL, or another status for which NT_SUCCESS(status) equals + FALSE otherwise. + +--*/ +{ + WDF_DRIVER_CONFIG config; + NTSTATUS status; + + KdPrint(("DriverEntry for VHidMini\n")); + +#ifdef _KERNEL_MODE + // + // Opt-in to using non-executable pool memory on Windows 8 and later. + // https://msdn.microsoft.com/en-us/library/windows/hardware/hh920402(v=vs.85).aspx + // + ExInitializeDriverRuntime(DrvRtPoolNxOptIn); +#endif + + WDF_DRIVER_CONFIG_INIT(&config, EvtDeviceAdd); + + status = WdfDriverCreate(DriverObject, + RegistryPath, + WDF_NO_OBJECT_ATTRIBUTES, + &config, + WDF_NO_HANDLE); + if (!NT_SUCCESS(status)) { + KdPrint(("Error: WdfDriverCreate failed 0x%x\n", status)); + return status; + } + + return status; +} + +NTSTATUS +EvtDeviceAdd( + _In_ WDFDRIVER Driver, + _Inout_ PWDFDEVICE_INIT DeviceInit + ) +/*++ +Routine Description: + + EvtDeviceAdd is called by the framework in response to AddDevice + call from the PnP manager. We create and initialize a device object to + represent a new instance of the device. + +Arguments: + + Driver - Handle to a framework driver object created in DriverEntry + + DeviceInit - Pointer to a framework-allocated WDFDEVICE_INIT structure. + +Return Value: + + NTSTATUS + +--*/ +{ + NTSTATUS status; + WDF_OBJECT_ATTRIBUTES deviceAttributes; + WDFDEVICE device; + PDEVICE_CONTEXT deviceContext; + PHID_DEVICE_ATTRIBUTES hidAttributes; + UNREFERENCED_PARAMETER (Driver); + + KdPrint(("Enter EvtDeviceAdd\n")); + + // + // Mark ourselves as a filter, which also relinquishes power policy ownership + // + WdfFdoInitSetFilter(DeviceInit); + + WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE( + &deviceAttributes, + DEVICE_CONTEXT); + + status = WdfDeviceCreate(&DeviceInit, + &deviceAttributes, + &device); + if (!NT_SUCCESS(status)) { + KdPrint(("Error: WdfDeviceCreate failed 0x%x\n", status)); + return status; + } + + deviceContext = GetDeviceContext(device); + deviceContext->Device = device; + deviceContext->DeviceData = 0; + + hidAttributes = &deviceContext->HidDeviceAttributes; + RtlZeroMemory(hidAttributes, sizeof(HID_DEVICE_ATTRIBUTES)); + hidAttributes->Size = sizeof(HID_DEVICE_ATTRIBUTES); + hidAttributes->VendorID = HIDMINI_VID; + hidAttributes->ProductID = HIDMINI_PID; + hidAttributes->VersionNumber = HIDMINI_VERSION; + + status = QueueCreate(device, + &deviceContext->DefaultQueue); + if( !NT_SUCCESS(status) ) { + return status; + } + + status = ManualQueueCreate(device, + &deviceContext->ManualQueue); + if( !NT_SUCCESS(status) ) { + return status; + } + + // + // Use default "HID Descriptor" (hardcoded). We will set the + // wReportLength memeber of HID descriptor when we read the + // the report descriptor either from registry or the hard-coded + // one. + // + deviceContext->HidDescriptor = G_DefaultHidDescriptor; + + // + // Check to see if we need to read the Report Descriptor from + // registry. If the "ReadFromRegistry" flag in the registry is set + // then we will read the descriptor from registry using routine + // ReadDescriptorFromRegistry(). Otherwise, we will use the + // hard-coded default report descriptor. + // + + status = CheckRegistryForDescriptor(device); + if (NT_SUCCESS(status)){ + // + // We need to read read descriptor from registry + // + status = ReadDescriptorFromRegistry(device); + if (!NT_SUCCESS(status)){ + KdPrint(("Failed to read descriptor from registry\n")); + } + } + + // + // We will use hard-coded report descriptor if registry one is not used. + // + if (!NT_SUCCESS(status)){ + deviceContext->ReportDescriptor = G_DefaultReportDescriptor; + KdPrint(("Using Hard-coded Report descriptor\n")); + status = STATUS_SUCCESS; + } + + return status; +} + +#ifdef _KERNEL_MODE +EVT_WDF_IO_QUEUE_IO_INTERNAL_DEVICE_CONTROL EvtIoDeviceControl; +#else +EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL EvtIoDeviceControl; +#endif + +NTSTATUS +QueueCreate( + _In_ WDFDEVICE Device, + _Out_ WDFQUEUE *Queue + ) +/*++ +Routine Description: + + This function creates a default, parallel I/O queue to proces IOCTLs + from hidclass.sys. + +Arguments: + + Device - Handle to a framework device object. + + Queue - Output pointer to a framework I/O queue handle, on success. + +Return Value: + + NTSTATUS + +--*/ +{ + NTSTATUS status; + WDF_IO_QUEUE_CONFIG queueConfig; + WDF_OBJECT_ATTRIBUTES queueAttributes; + WDFQUEUE queue; + PQUEUE_CONTEXT queueContext; + + WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE( + &queueConfig, + WdfIoQueueDispatchParallel); + +#ifdef _KERNEL_MODE + queueConfig.EvtIoInternalDeviceControl = EvtIoDeviceControl; +#else + // + // HIDclass uses INTERNAL_IOCTL which is not supported by UMDF. Therefore + // the hidumdf.sys changes the IOCTL type to DEVICE_CONTROL for next stack + // and sends it down + // + queueConfig.EvtIoDeviceControl = EvtIoDeviceControl; +#endif + + WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE( + &queueAttributes, + QUEUE_CONTEXT); + + status = WdfIoQueueCreate( + Device, + &queueConfig, + &queueAttributes, + &queue); + + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfIoQueueCreate failed 0x%x\n",status)); + return status; + } + + queueContext = GetQueueContext(queue); + queueContext->Queue = queue; + queueContext->DeviceContext = GetDeviceContext(Device); + queueContext->OutputReport = 0; + + *Queue = queue; + return status; +} + +VOID +EvtIoDeviceControl( + _In_ WDFQUEUE Queue, + _In_ WDFREQUEST Request, + _In_ size_t OutputBufferLength, + _In_ size_t InputBufferLength, + _In_ ULONG IoControlCode + ) +/*++ +Routine Description: + + This event callback function is called when the driver receives an + + (KMDF) IOCTL_HID_Xxx code when handlng IRP_MJ_INTERNAL_DEVICE_CONTROL + (UMDF) IOCTL_HID_Xxx, IOCTL_UMDF_HID_Xxx when handling IRP_MJ_DEVICE_CONTROL + +Arguments: + + Queue - A handle to the queue object that is associated with the I/O request + + Request - A handle to a framework request object. + + OutputBufferLength - The length, in bytes, of the request's output buffer, + if an output buffer is available. + + InputBufferLength - The length, in bytes, of the request's input buffer, if + an input buffer is available. + + IoControlCode - The driver or system defined IOCTL associated with the request + +Return Value: + + NTSTATUS + +--*/ +{ + NTSTATUS status; + BOOLEAN completeRequest = TRUE; + WDFDEVICE device = WdfIoQueueGetDevice(Queue); + PDEVICE_CONTEXT deviceContext = NULL; + PQUEUE_CONTEXT queueContext = GetQueueContext(Queue); + UNREFERENCED_PARAMETER (OutputBufferLength); + UNREFERENCED_PARAMETER (InputBufferLength); + + deviceContext = GetDeviceContext(device); + + switch (IoControlCode) + { + case IOCTL_HID_GET_DEVICE_DESCRIPTOR: // METHOD_NEITHER + // + // Retrieves the device's HID descriptor. + // + _Analysis_assume_(deviceContext->HidDescriptor.bLength != 0); + status = RequestCopyFromBuffer(Request, + &deviceContext->HidDescriptor, + deviceContext->HidDescriptor.bLength); + break; + + case IOCTL_HID_GET_DEVICE_ATTRIBUTES: // METHOD_NEITHER + // + //Retrieves a device's attributes in a HID_DEVICE_ATTRIBUTES structure. + // + status = RequestCopyFromBuffer(Request, + &queueContext->DeviceContext->HidDeviceAttributes, + sizeof(HID_DEVICE_ATTRIBUTES)); + break; + + case IOCTL_HID_GET_REPORT_DESCRIPTOR: // METHOD_NEITHER + // + //Obtains the report descriptor for the HID device. + // + status = RequestCopyFromBuffer(Request, + deviceContext->ReportDescriptor, + deviceContext->HidDescriptor.DescriptorList[0].wReportLength); + break; + + case IOCTL_HID_READ_REPORT: // METHOD_NEITHER + // + // Returns a report from the device into a class driver-supplied + // buffer. + // + status = ReadReport(queueContext, Request, &completeRequest); + break; + + case IOCTL_HID_WRITE_REPORT: // METHOD_NEITHER + // + // Transmits a class driver-supplied report to the device. + // + status = WriteReport(queueContext, Request); + break; + +#ifdef _KERNEL_MODE + + case IOCTL_HID_GET_FEATURE: // METHOD_OUT_DIRECT + + status = GetFeature(queueContext, Request); + break; + + case IOCTL_HID_SET_FEATURE: // METHOD_IN_DIRECT + + status = SetFeature(queueContext, Request); + break; + + case IOCTL_HID_GET_INPUT_REPORT: // METHOD_OUT_DIRECT + + status = GetInputReport(queueContext, Request); + break; + + case IOCTL_HID_SET_OUTPUT_REPORT: // METHOD_IN_DIRECT + + status = SetOutputReport(queueContext, Request); + break; + +#else // UMDF specific + + // + // HID minidriver IOCTL uses HID_XFER_PACKET which contains an embedded pointer. + // + // typedef struct _HID_XFER_PACKET { + // PUCHAR reportBuffer; + // ULONG reportBufferLen; + // UCHAR reportId; + // } HID_XFER_PACKET, *PHID_XFER_PACKET; + // + // UMDF cannot handle embedded pointers when marshalling buffers between processes. + // Therefore a special driver mshidumdf.sys is introduced to convert such IRPs to + // new IRPs (with new IOCTL name like IOCTL_UMDF_HID_Xxxx) where: + // + // reportBuffer - passed as one buffer inside the IRP + // reportId - passed as a second buffer inside the IRP + // + // The new IRP is then passed to UMDF host and driver for further processing. + // + + case IOCTL_UMDF_HID_GET_FEATURE: // METHOD_NEITHER + + status = GetFeature(queueContext, Request); + break; + + case IOCTL_UMDF_HID_SET_FEATURE: // METHOD_NEITHER + + status = SetFeature(queueContext, Request); + break; + + case IOCTL_UMDF_HID_GET_INPUT_REPORT: // METHOD_NEITHER + + status = GetInputReport(queueContext, Request); + break; + + case IOCTL_UMDF_HID_SET_OUTPUT_REPORT: // METHOD_NEITHER + + status = SetOutputReport(queueContext, Request); + break; + +#endif // _KERNEL_MODE + + case IOCTL_HID_GET_STRING: // METHOD_NEITHER + + status = GetString(Request); + break; + + case IOCTL_HID_GET_INDEXED_STRING: // METHOD_OUT_DIRECT + + status = GetIndexedString(Request); + break; + + case IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST: // METHOD_NEITHER + // + // This has the USBSS Idle notification callback. If the lower driver + // can handle it (e.g. USB stack can handle it) then pass it down + // otherwise complete it here as not inplemented. For a virtual + // device, idling is not needed. + // + // Not implemented. fall through... + // + case IOCTL_HID_ACTIVATE_DEVICE: // METHOD_NEITHER + case IOCTL_HID_DEACTIVATE_DEVICE: // METHOD_NEITHER + case IOCTL_GET_PHYSICAL_DESCRIPTOR: // METHOD_OUT_DIRECT + // + // We don't do anything for these IOCTLs but some minidrivers might. + // + // Not implemented. fall through... + // + default: + status = STATUS_NOT_IMPLEMENTED; + break; + } + + // + // Complete the request. Information value has already been set by request + // handlers. + // + if (completeRequest) { + WdfRequestComplete(Request, status); + } +} + +NTSTATUS +RequestCopyFromBuffer( + _In_ WDFREQUEST Request, + _In_ PVOID SourceBuffer, + _When_(NumBytesToCopyFrom == 0, __drv_reportError(NumBytesToCopyFrom cannot be zero)) + _In_ size_t NumBytesToCopyFrom + ) +/*++ + +Routine Description: + + A helper function to copy specified bytes to the request's output memory + +Arguments: + + Request - A handle to a framework request object. + + SourceBuffer - The buffer to copy data from. + + NumBytesToCopyFrom - The length, in bytes, of data to be copied. + +Return Value: + + NTSTATUS + +--*/ +{ + NTSTATUS status; + WDFMEMORY memory; + size_t outputBufferLength; + + status = WdfRequestRetrieveOutputMemory(Request, &memory); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfRequestRetrieveOutputMemory failed 0x%x\n",status)); + return status; + } + + WdfMemoryGetBuffer(memory, &outputBufferLength); + if (outputBufferLength < NumBytesToCopyFrom) { + status = STATUS_INVALID_BUFFER_SIZE; + KdPrint(("RequestCopyFromBuffer: buffer too small. Size %d, expect %d\n", + (int)outputBufferLength, (int)NumBytesToCopyFrom)); + return status; + } + + status = WdfMemoryCopyFromBuffer(memory, + 0, + SourceBuffer, + NumBytesToCopyFrom); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfMemoryCopyFromBuffer failed 0x%x\n",status)); + return status; + } + + WdfRequestSetInformation(Request, NumBytesToCopyFrom); + return status; +} + +NTSTATUS +ReadReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request, + _Always_(_Out_) + BOOLEAN* CompleteRequest + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_READ_REPORT for the HID collection. Normally the request + will be forwarded to a manual queue for further process. In that case, the + caller should not try to complete the request at this time, as the request + will later be retrieved back from the manually queue and completed there. + However, if for some reason the forwarding fails, the caller still need + to complete the request with proper error code immediately. + +Arguments: + + QueueContext - The object context associated with the queue + + Request - Pointer to Request Packet. + + CompleteRequest - A boolean output value, indicating whether the caller + should complete the request or not + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + + KdPrint(("ReadReport\n")); + + // + // forward the request to manual queue + // + status = WdfRequestForwardToIoQueue( + Request, + QueueContext->DeviceContext->ManualQueue); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfRequestForwardToIoQueue failed with 0x%x\n", status)); + *CompleteRequest = TRUE; + } + else { + *CompleteRequest = FALSE; + } + + return status; +} + +NTSTATUS +WriteReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_WRITE_REPORT all the collection. + +Arguments: + + QueueContext - The object context associated with the queue + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ + +{ + NTSTATUS status; + HID_XFER_PACKET packet; + ULONG reportSize; + PHIDMINI_OUTPUT_REPORT outputReport; + + KdPrint(("WriteReport\n")); + + status = RequestGetHidXferPacket_ToWriteToDevice( + Request, + &packet); + if( !NT_SUCCESS(status) ) { + return status; + } + + if (packet.reportId != CONTROL_COLLECTION_REPORT_ID) { + // + // Return error for unknown collection + // + status = STATUS_INVALID_PARAMETER; + KdPrint(("WriteReport: unkown report id %d\n", packet.reportId)); + return status; + } + + // + // before touching buffer make sure buffer is big enough. + // + reportSize = sizeof(HIDMINI_OUTPUT_REPORT); + + if (packet.reportBufferLen < reportSize) { + status = STATUS_INVALID_BUFFER_SIZE; + KdPrint(("WriteReport: invalid input buffer. size %d, expect %d\n", + packet.reportBufferLen, reportSize)); + return status; + } + + outputReport = (PHIDMINI_OUTPUT_REPORT)packet.reportBuffer; + + // + // Store the device data in device extension. + // + QueueContext->DeviceContext->DeviceData = outputReport->Data; + + // + // set status and information + // + WdfRequestSetInformation(Request, reportSize); + return status; +} + + +HRESULT +GetFeature( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_GET_FEATURE for all the collection. + +Arguments: + + QueueContext - The object context associated with the queue + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + HID_XFER_PACKET packet; + ULONG reportSize; + PMY_DEVICE_ATTRIBUTES myAttributes; + PHID_DEVICE_ATTRIBUTES hidAttributes = &QueueContext->DeviceContext->HidDeviceAttributes; + + KdPrint(("GetFeature\n")); + + status = RequestGetHidXferPacket_ToReadFromDevice( + Request, + &packet); + if( !NT_SUCCESS(status) ) { + return status; + } + + if (packet.reportId != CONTROL_COLLECTION_REPORT_ID) { + // + // If collection ID is not for control collection then handle + // this request just as you would for a regular collection. + // + status = STATUS_INVALID_PARAMETER; + KdPrint(("GetFeature: invalid report id %d\n", packet.reportId)); + return status; + } + + // + // Since output buffer is for write only (no read allowed by UMDF in output + // buffer), any read from output buffer would be reading garbage), so don't + // let app embed custom control code in output buffer. The minidriver can + // support multiple features using separate report ID instead of using + // custom control code. Since this is targeted at report ID 1, we know it + // is a request for getting attributes. + // + // While KMDF does not enforce the rule (disallow read from output buffer), + // it is good practice to not do so. + // + + reportSize = sizeof(MY_DEVICE_ATTRIBUTES) + sizeof(packet.reportId); + if (packet.reportBufferLen < reportSize) { + status = STATUS_INVALID_BUFFER_SIZE; + KdPrint(("GetFeature: output buffer too small. Size %d, expect %d\n", + packet.reportBufferLen, reportSize)); + return status; + } + + // + // Since this device has one report ID, hidclass would pass on the report + // ID in the buffer (it wouldn't if report descriptor did not have any report + // ID). However, since UMDF allows only writes to an output buffer, we can't + // "read" the report ID from "output" buffer. There is no need to read the + // report ID since we get it other way as shown above, however this is + // something to keep in mind. + // + myAttributes = (PMY_DEVICE_ATTRIBUTES)(packet.reportBuffer + sizeof(packet.reportId)); + myAttributes->ProductID = hidAttributes->ProductID; + myAttributes->VendorID = hidAttributes->VendorID; + myAttributes->VersionNumber = hidAttributes->VersionNumber; + + // + // Report how many bytes were copied + // + WdfRequestSetInformation(Request, reportSize); + return status; +} + +NTSTATUS +SetFeature( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_SET_FEATURE for all the collection. + For control collection (custom defined collection) it handles + the user-defined control codes for sideband communication + +Arguments: + + QueueContext - The object context associated with the queue + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + WDFMEMORY inputMemory; + size_t inputLen = 0; + PUCHAR inputBuf; + size_t i; + size_t scan; + + KdPrint(("SetFeature\n")); + + // + // Read the feature report data directly. We deliberately do NOT use + // RequestGetHidXferPacket_ToWriteToDevice() here: that helper derives the + // report id from the request's *output* buffer length, which is 0 for an + // unnumbered device (report id 0) and makes the retrieval fail. We only + // need the raw bytes. + // + status = WdfRequestRetrieveInputMemory(Request, &inputMemory); + if (!NT_SUCCESS(status)) { + return status; + } + inputBuf = (PUCHAR)WdfMemoryGetBuffer(inputMemory, &inputLen); + + // + // HIDAPI test: a Feature report carries a scenario command. Depending on + // whether the report is prefixed by a report-id byte, the command is in the + // first couple of bytes; scan for a recognised TEST_VDEV_CMD_* value (the + // report-id byte is 0 and the commands are non-zero, so this is + // unambiguous). The matching input report is replayed by the manual-queue + // timer (EvtTimerFunc). + // + scan = inputLen; + if (scan > 4) { + scan = 4; + } + for (i = 0; i < scan; i++) { + UCHAR b = inputBuf[i]; + if (b == TEST_VDEV_CMD_EMIT_A || b == TEST_VDEV_CMD_EMIT_B) { + InterlockedExchange(&QueueContext->DeviceContext->ScenarioCommand, (LONG)b); + break; + } + } + + WdfRequestSetInformation(Request, inputLen); + return STATUS_SUCCESS; +} + +NTSTATUS +GetInputReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_GET_INPUT_REPORT for all the collection. + +Arguments: + + QueueContext - The object context associated with the queue + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + HID_XFER_PACKET packet; + ULONG reportSize; + PHIDMINI_INPUT_REPORT reportBuffer; + + KdPrint(("GetInputReport\n")); + + status = RequestGetHidXferPacket_ToReadFromDevice( + Request, + &packet); + if( !NT_SUCCESS(status) ) { + return status; + } + + if (packet.reportId != CONTROL_COLLECTION_REPORT_ID) { + // + // If collection ID is not for control collection then handle + // this request just as you would for a regular collection. + // + status = STATUS_INVALID_PARAMETER; + KdPrint(("GetInputReport: invalid report id %d\n", packet.reportId)); + return status; + } + + reportSize = sizeof(HIDMINI_INPUT_REPORT); + if (packet.reportBufferLen < reportSize) { + status = STATUS_INVALID_BUFFER_SIZE; + KdPrint(("GetInputReport: output buffer too small. Size %d, expect %d\n", + packet.reportBufferLen, reportSize)); + return status; + } + + reportBuffer = (PHIDMINI_INPUT_REPORT)(packet.reportBuffer); + + reportBuffer->ReportId = CONTROL_COLLECTION_REPORT_ID; + reportBuffer->Data = QueueContext->OutputReport; + + // + // Report how many bytes were copied + // + WdfRequestSetInformation(Request, reportSize); + return status; +} + + +NTSTATUS +SetOutputReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_SET_OUTPUT_REPORT for all the collection. + +Arguments: + + QueueContext - The object context associated with the queue + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + HID_XFER_PACKET packet; + ULONG reportSize; + PHIDMINI_OUTPUT_REPORT reportBuffer; + + KdPrint(("SetOutputReport\n")); + + status = RequestGetHidXferPacket_ToWriteToDevice( + Request, + &packet); + if( !NT_SUCCESS(status) ) { + return status; + } + + if (packet.reportId != CONTROL_COLLECTION_REPORT_ID) { + // + // If collection ID is not for control collection then handle + // this request just as you would for a regular collection. + // + status = STATUS_INVALID_PARAMETER; + KdPrint(("SetOutputReport: unkown report id %d\n", packet.reportId)); + return status; + } + + // + // before touching buffer make sure buffer is big enough. + // + reportSize = sizeof(HIDMINI_OUTPUT_REPORT); + + if (packet.reportBufferLen < reportSize) { + status = STATUS_INVALID_BUFFER_SIZE; + KdPrint(("SetOutputReport: invalid input buffer. size %d, expect %d\n", + packet.reportBufferLen, reportSize)); + return status; + } + + reportBuffer = (PHIDMINI_OUTPUT_REPORT)packet.reportBuffer; + + QueueContext->OutputReport = reportBuffer->Data; + + // + // Report how many bytes were copied + // + WdfRequestSetInformation(Request, reportSize); + return status; +} + + +NTSTATUS +GetStringId( + _In_ WDFREQUEST Request, + _Out_ ULONG *StringId, + _Out_ ULONG *LanguageId + ) +/*++ + +Routine Description: + + Helper routine to decode IOCTL_HID_GET_INDEXED_STRING and IOCTL_HID_GET_STRING. + +Arguments: + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + ULONG inputValue; + +#ifdef _KERNEL_MODE + + WDF_REQUEST_PARAMETERS requestParameters; + + // + // IOCTL_HID_GET_STRING: // METHOD_NEITHER + // IOCTL_HID_GET_INDEXED_STRING: // METHOD_OUT_DIRECT + // + // The string id (or string index) is passed in Parameters.DeviceIoControl. + // Type3InputBuffer. However, Parameters.DeviceIoControl.InputBufferLength + // was not initialized by hidclass.sys, therefore trying to access the + // buffer with WdfRequestRetrieveInputMemory will fail + // + // Another problem with IOCTL_HID_GET_INDEXED_STRING is that METHOD_OUT_DIRECT + // expects the input buffer to be Irp->AssociatedIrp.SystemBuffer instead of + // Type3InputBuffer. That will also fail WdfRequestRetrieveInputMemory. + // + // The solution to the above two problems is to get Type3InputBuffer directly + // + // Also note that instead of the buffer's content, it is the buffer address + // that was used to store the string id (or index) + // + + WDF_REQUEST_PARAMETERS_INIT(&requestParameters); + WdfRequestGetParameters(Request, &requestParameters); + + inputValue = PtrToUlong( + requestParameters.Parameters.DeviceIoControl.Type3InputBuffer); + + status = STATUS_SUCCESS; + +#else + + WDFMEMORY inputMemory; + size_t inputBufferLength; + PVOID inputBuffer; + + // + // mshidumdf.sys updates the IRP and passes the string id (or index) through + // the input buffer correctly based on the IOCTL buffer type + // + + status = WdfRequestRetrieveInputMemory(Request, &inputMemory); + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfRequestRetrieveInputMemory failed 0x%x\n",status)); + return status; + } + inputBuffer = WdfMemoryGetBuffer(inputMemory, &inputBufferLength); + + // + // make sure buffer is big enough. + // + if (inputBufferLength < sizeof(ULONG)) + { + status = STATUS_INVALID_BUFFER_SIZE; + KdPrint(("GetStringId: invalid input buffer. size %d, expect %d\n", + (int)inputBufferLength, (int)sizeof(ULONG))); + return status; + } + + inputValue = (*(PULONG)inputBuffer); + +#endif + + // + // The least significant two bytes of the INT value contain the string id. + // + *StringId = (inputValue & 0x0ffff); + + // + // The most significant two bytes of the INT value contain the language + // ID (for example, a value of 1033 indicates English). + // + *LanguageId = (inputValue >> 16); + + return status; +} + + +NTSTATUS +GetIndexedString( + _In_ WDFREQUEST Request + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_GET_INDEXED_STRING + +Arguments: + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + ULONG languageId, stringIndex; + + status = GetStringId(Request, &stringIndex, &languageId); + + // While we don't use the language id, some minidrivers might. + // + UNREFERENCED_PARAMETER(languageId); + + if (NT_SUCCESS(status)) { + + if (stringIndex != VHIDMINI_DEVICE_STRING_INDEX) + { + status = STATUS_INVALID_PARAMETER; + KdPrint(("GetString: unkown string index %d\n", stringIndex)); + return status; + } + + status = RequestCopyFromBuffer(Request, VHIDMINI_DEVICE_STRING, sizeof(VHIDMINI_DEVICE_STRING)); + } + return status; +} + + +NTSTATUS +GetString( + _In_ WDFREQUEST Request + ) +/*++ + +Routine Description: + + Handles IOCTL_HID_GET_STRING. + +Arguments: + + Request - Pointer to Request Packet. + +Return Value: + + NT status code. + +--*/ +{ + NTSTATUS status; + ULONG languageId, stringId; + size_t stringSizeCb; + PWSTR string; + + status = GetStringId(Request, &stringId, &languageId); + + // While we don't use the language id, some minidrivers might. + // + UNREFERENCED_PARAMETER(languageId); + + if (!NT_SUCCESS(status)) { + return status; + } + + switch (stringId){ + case HID_STRING_ID_IMANUFACTURER: + stringSizeCb = sizeof(VHIDMINI_MANUFACTURER_STRING); + string = VHIDMINI_MANUFACTURER_STRING; + break; + case HID_STRING_ID_IPRODUCT: + stringSizeCb = sizeof(VHIDMINI_PRODUCT_STRING); + string = VHIDMINI_PRODUCT_STRING; + break; + case HID_STRING_ID_ISERIALNUMBER: + stringSizeCb = sizeof(VHIDMINI_SERIAL_NUMBER_STRING); + string = VHIDMINI_SERIAL_NUMBER_STRING; + break; + default: + status = STATUS_INVALID_PARAMETER; + KdPrint(("GetString: unkown string id %d\n", stringId)); + return status; + } + + status = RequestCopyFromBuffer(Request, string, stringSizeCb); + return status; +} + + +NTSTATUS +ManualQueueCreate( + _In_ WDFDEVICE Device, + _Out_ WDFQUEUE *Queue + ) +/*++ +Routine Description: + + This function creates a manual I/O queue to receive IOCTL_HID_READ_REPORT + forwarded from the device's default queue handler. + + It also creates a periodic timer to check the queue and complete any pending + request with data from the device. Here timer expiring is used to simulate + a hardware event that new data is ready. + + The workflow is like this: + + - Hidclass.sys sends an ioctl to the miniport to read input report. + + - The request reaches the driver's default queue. As data may not be avaiable + yet, the request is forwarded to a second manual queue temporarily. + + - Later when data is ready (as simulated by timer expiring), the driver + checks for any pending request in the manual queue, and then completes it. + + - Hidclass gets notified for the read request completion and return data to + the caller. + + On the other hand, for IOCTL_HID_WRITE_REPORT request, the driver simply + sends the request to the hardware (as simulated by storing the data at + DeviceContext->DeviceData) and completes the request immediately. There is + no need to use another queue for write operation. + +Arguments: + + Device - Handle to a framework device object. + + Queue - Output pointer to a framework I/O queue handle, on success. + +Return Value: + + NTSTATUS + +--*/ +{ + NTSTATUS status; + WDF_IO_QUEUE_CONFIG queueConfig; + WDF_OBJECT_ATTRIBUTES queueAttributes; + WDFQUEUE queue; + PMANUAL_QUEUE_CONTEXT queueContext; + WDF_TIMER_CONFIG timerConfig; + WDF_OBJECT_ATTRIBUTES timerAttributes; + ULONG timerPeriodInSeconds = 5; + + WDF_IO_QUEUE_CONFIG_INIT( + &queueConfig, + WdfIoQueueDispatchManual); + + WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE( + &queueAttributes, + MANUAL_QUEUE_CONTEXT); + + status = WdfIoQueueCreate( + Device, + &queueConfig, + &queueAttributes, + &queue); + + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfIoQueueCreate failed 0x%x\n",status)); + return status; + } + + queueContext = GetManualQueueContext(queue); + queueContext->Queue = queue; + queueContext->DeviceContext = GetDeviceContext(Device); + + /* HIDAPI test: poll the scenario state often so a triggered input report + is replayed promptly (well within the test's read timeouts). */ + (void)timerPeriodInSeconds; + WDF_TIMER_CONFIG_INIT_PERIODIC( + &timerConfig, + EvtTimerFunc, + 50 /* ms */); + + WDF_OBJECT_ATTRIBUTES_INIT(&timerAttributes); + timerAttributes.ParentObject = queue; + status = WdfTimerCreate(&timerConfig, + &timerAttributes, + &queueContext->Timer); + + if( !NT_SUCCESS(status) ) { + KdPrint(("WdfTimerCreate failed 0x%x\n",status)); + return status; + } + + WdfTimerStart(queueContext->Timer, WDF_REL_TIMEOUT_IN_MS(50)); + + *Queue = queue; + + return status; +} + +void +EvtTimerFunc( + _In_ WDFTIMER Timer + ) +/*++ +Routine Description: + + This periodic timer callback routine checks the device's manual queue and + completes any pending request with data from the device. + +Arguments: + + Timer - Handle to a timer object that was obtained from WdfTimerCreate. + +Return Value: + + VOID + +--*/ +{ + NTSTATUS status; + WDFQUEUE queue; + PMANUAL_QUEUE_CONTEXT queueContext; + WDFREQUEST request; + LONG command; + const UCHAR *payload; + static const UCHAR inputA[TEST_VDEV_REPORT_SIZE] = { TEST_VDEV_INPUT_A_BYTES }; + static const UCHAR inputB[TEST_VDEV_REPORT_SIZE] = { TEST_VDEV_INPUT_B_BYTES }; + + queue = (WDFQUEUE)WdfTimerGetParentObject(Timer); + queueContext = GetManualQueueContext(queue); + + // + // HIDAPI test: only deliver an input report when a scenario has been + // triggered via SetFeature. Otherwise leave pended reads alone, so that a + // blocking hid_read() genuinely blocks until hid_read_interrupt() cancels + // it (the read-interrupt tests rely on the device being silent). + // + command = InterlockedExchange(&queueContext->DeviceContext->ScenarioCommand, 0); + if (command == 0) { + return; + } + + status = WdfIoQueueRetrieveNextRequest(queueContext->Queue, &request); + if (!NT_SUCCESS(status)) { + // + // The read hasn't been pended yet; re-arm the command so a later timer + // tick retries once hid_read() has issued the request. + // + InterlockedExchange(&queueContext->DeviceContext->ScenarioCommand, command); + return; + } + + /* Deliver the raw report bytes (no report-id prefix: the device is + unnumbered, so the report on the wire is just the payload). hidclass + frames it for user mode; HIDAPI returns exactly these bytes. */ + payload = (command == TEST_VDEV_CMD_EMIT_B) ? inputB : inputA; + status = RequestCopyFromBuffer(request, (PVOID)payload, TEST_VDEV_REPORT_SIZE); + WdfRequestComplete(request, status); +} + +NTSTATUS +CheckRegistryForDescriptor( + WDFDEVICE Device + ) +/*++ + +Routine Description: + + Read "ReadFromRegistry" key value from device parameters in the registry. + +Arguments: + + device - pointer to a device object. + +Return Value: + + NT status code. + +--*/ + +{ + WDFKEY hKey = NULL; + NTSTATUS status; + UNICODE_STRING valueName; + ULONG value; + + status = WdfDeviceOpenRegistryKey(Device, + PLUGPLAY_REGKEY_DEVICE, + KEY_READ, + WDF_NO_OBJECT_ATTRIBUTES, + &hKey); + if (NT_SUCCESS(status)) { + + RtlInitUnicodeString(&valueName, L"ReadFromRegistry"); + + status = WdfRegistryQueryULong (hKey, + &valueName, + &value); + + if (NT_SUCCESS (status)) { + if (value == 0) { + status = STATUS_UNSUCCESSFUL; + } + } + + WdfRegistryClose(hKey); + } + + return status; +} + +NTSTATUS +ReadDescriptorFromRegistry( + WDFDEVICE Device + ) +/*++ + +Routine Description: + + Read HID report descriptor from registry + +Arguments: + + device - pointer to a device object. + +Return Value: + + NT status code. + +--*/ +{ + WDFKEY hKey = NULL; + NTSTATUS status; + UNICODE_STRING valueName; + WDFMEMORY memory; + size_t bufferSize; + PVOID reportDescriptor; + PDEVICE_CONTEXT deviceContext; + WDF_OBJECT_ATTRIBUTES attributes; + + deviceContext = GetDeviceContext(Device); + + status = WdfDeviceOpenRegistryKey(Device, + PLUGPLAY_REGKEY_DEVICE, + KEY_READ, + WDF_NO_OBJECT_ATTRIBUTES, + &hKey); + + if (NT_SUCCESS(status)) { + + RtlInitUnicodeString(&valueName, L"MyReportDescriptor"); + + WDF_OBJECT_ATTRIBUTES_INIT(&attributes); + attributes.ParentObject = Device; + + status = WdfRegistryQueryMemory (hKey, + &valueName, + NonPagedPool, + &attributes, + &memory, + NULL); + + if (NT_SUCCESS (status)) { + + reportDescriptor = WdfMemoryGetBuffer(memory, &bufferSize); + + KdPrint(("No. of report descriptor bytes copied: %d\n", (INT) bufferSize)); + + // + // Store the registry report descriptor in the device extension + // + deviceContext->ReadReportDescFromRegistry = TRUE; + deviceContext->ReportDescriptor = reportDescriptor; + deviceContext->HidDescriptor.DescriptorList[0].wReportLength = (USHORT)bufferSize; + } + + WdfRegistryClose(hKey); + } + + return status; +} + diff --git a/src/tests/windows/driver/vhidmini.h b/src/tests/windows/driver/vhidmini.h new file mode 100644 index 00000000..9dc03750 --- /dev/null +++ b/src/tests/windows/driver/vhidmini.h @@ -0,0 +1,208 @@ +/*++ + +Copyright (C) Microsoft Corporation, All Rights Reserved + +Module Name: + + vhidmini.h + +Abstract: + + This module contains the type definitions for the driver + +Environment: + + Windows Driver Framework (WDF) + +--*/ + +/* + * Modified by the libusb/hidapi team for the HIDAPI virtual-device tests + * (implements the HIDAPI "scenario" protocol; see + * src/tests/test_virtual_device.h). Derived from the vhidmini2 sample in + * microsoft/Windows-driver-samples, which is licensed under the Microsoft + * Public License (MS-PL); see README.md and LICENSE.txt in this directory. + */ + +#ifdef _KERNEL_MODE +#include +#else +#include +#endif + +#include + +#include // located in $(DDK_INC_PATH)/wdm + +#include "common.h" + +typedef UCHAR HID_REPORT_DESCRIPTOR, *PHID_REPORT_DESCRIPTOR; + +DRIVER_INITIALIZE DriverEntry; +EVT_WDF_DRIVER_DEVICE_ADD EvtDeviceAdd; +EVT_WDF_TIMER EvtTimerFunc; + +typedef struct _DEVICE_CONTEXT +{ + WDFDEVICE Device; + WDFQUEUE DefaultQueue; + WDFQUEUE ManualQueue; + HID_DEVICE_ATTRIBUTES HidDeviceAttributes; + BYTE DeviceData; + HID_DESCRIPTOR HidDescriptor; + PHID_REPORT_DESCRIPTOR ReportDescriptor; + BOOLEAN ReadReportDescFromRegistry; + + // + // HIDAPI test scenario state. SetFeature() stores the requested scenario + // command here (see test_virtual_device.h); the manual-queue timer replays + // the matching pre-recorded input report and clears it. Accessed from the + // IO thread and the timer thread via Interlocked*. + // + volatile LONG ScenarioCommand; + +} DEVICE_CONTEXT, *PDEVICE_CONTEXT; + +WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DEVICE_CONTEXT, GetDeviceContext); + +typedef struct _QUEUE_CONTEXT +{ + WDFQUEUE Queue; + PDEVICE_CONTEXT DeviceContext; + UCHAR OutputReport; + +} QUEUE_CONTEXT, *PQUEUE_CONTEXT; + +WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(QUEUE_CONTEXT, GetQueueContext); + +NTSTATUS +QueueCreate( + _In_ WDFDEVICE Device, + _Out_ WDFQUEUE *Queue + ); + +typedef struct _MANUAL_QUEUE_CONTEXT +{ + WDFQUEUE Queue; + PDEVICE_CONTEXT DeviceContext; + WDFTIMER Timer; + +} MANUAL_QUEUE_CONTEXT, *PMANUAL_QUEUE_CONTEXT; + +WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(MANUAL_QUEUE_CONTEXT, GetManualQueueContext); + +NTSTATUS +ManualQueueCreate( + _In_ WDFDEVICE Device, + _Out_ WDFQUEUE *Queue + ); + +NTSTATUS +ReadReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request, + _Always_(_Out_) + BOOLEAN* CompleteRequest + ); + +NTSTATUS +WriteReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ); + +NTSTATUS +GetFeature( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ); + +NTSTATUS +SetFeature( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ); + +NTSTATUS +GetInputReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ); + +NTSTATUS +SetOutputReport( + _In_ PQUEUE_CONTEXT QueueContext, + _In_ WDFREQUEST Request + ); + +NTSTATUS +GetString( + _In_ WDFREQUEST Request + ); + +NTSTATUS +GetIndexedString( + _In_ WDFREQUEST Request + ); + +NTSTATUS +GetStringId( + _In_ WDFREQUEST Request, + _Out_ ULONG *StringId, + _Out_ ULONG *LanguageId + ); + +NTSTATUS +RequestCopyFromBuffer( + _In_ WDFREQUEST Request, + _In_ PVOID SourceBuffer, + _When_(NumBytesToCopyFrom == 0, __drv_reportError(NumBytesToCopyFrom cannot be zero)) + _In_ size_t NumBytesToCopyFrom + ); + +NTSTATUS +RequestGetHidXferPacket_ToReadFromDevice( + _In_ WDFREQUEST Request, + _Out_ HID_XFER_PACKET *Packet + ); + +NTSTATUS +RequestGetHidXferPacket_ToWriteToDevice( + _In_ WDFREQUEST Request, + _Out_ HID_XFER_PACKET *Packet + ); + +NTSTATUS +CheckRegistryForDescriptor( + _In_ WDFDEVICE Device + ); + +NTSTATUS +ReadDescriptorFromRegistry( + _In_ WDFDEVICE Device + ); + +// +// Misc definitions +// +#define CONTROL_FEATURE_REPORT_ID 0x01 + +// +// These are the device attributes returned by the mini driver in response +// to IOCTL_HID_GET_DEVICE_ATTRIBUTES. +// +// VID/PID match the HIDAPI test (see src/tests/test_read_interrupt.c). +#define HIDMINI_PID 0x9001 +#define HIDMINI_VID 0xF1D0 +#define HIDMINI_VERSION 0x0101 + +// +// HIDAPI test scenario protocol (kept in sync with src/tests/test_virtual_device.h). +// A Feature SET_REPORT whose payload contains one of these commands makes the +// device replay the matching 8-byte input report. +// +#define TEST_VDEV_REPORT_SIZE 8 +#define TEST_VDEV_CMD_EMIT_A 0x01 +#define TEST_VDEV_CMD_EMIT_B 0x02 +#define TEST_VDEV_INPUT_A_BYTES 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8 +#define TEST_VDEV_INPUT_B_BYTES 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8 diff --git a/src/tests/windows/driver/vhidmini.rc b/src/tests/windows/driver/vhidmini.rc new file mode 100644 index 00000000..3721ac98 --- /dev/null +++ b/src/tests/windows/driver/vhidmini.rc @@ -0,0 +1,16 @@ +//--------------------------------------------------------------------------- +// vhidmini.rc +// +// Copyright (c) Microsoft Corporation, All Rights Reserved +//--------------------------------------------------------------------------- + + +#include +#include + +#define VER_FILETYPE VFT_DLL +#define VER_FILESUBTYPE VFT_UNKNOWN +#define VER_FILEDESCRIPTION_STR "HID minidriver (UMDF 2 version)" +#define VER_INTERNALNAME_STR "VhidminiUm.dll" + +#include "common.ver"