diff --git a/switch2_commands.cpp b/switch2_commands.cpp new file mode 100644 index 0000000..de8716f --- /dev/null +++ b/switch2_commands.cpp @@ -0,0 +1,79 @@ +#include "switch2_commands.h" + +#include + +// Captured command vectors and acknowledgements: +// https://github.com/ndeadly/switch2_controller_research/blob/d1c5a7f7ba298f83017fae84952a4e6d2ef8fc92/commands.md +namespace { + +constexpr uint8_t kSelectReportResponse[] = { + 0x03, 0x01, 0x00, 0x0A, 0x00, 0xF8, 0x00, 0x00, +}; +constexpr uint8_t kInitializeUsbResponse[] = { + 0x03, 0x01, 0x00, 0x0D, 0x00, 0xF8, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, +}; + +} // namespace + +Switch2VendorCommand switch2_classify_vendor_request( + const uint8_t* data, + std::size_t length) { + if (data == nullptr || length < 8) { + return Switch2VendorCommand::Unsupported; + } + if (data[0] != 0x03 || data[1] != 0x91 || data[2] != 0x00 || + data[4] != 0x00 || data[6] != 0x00 || data[7] != 0x00) { + return Switch2VendorCommand::Unsupported; + } + if (length != static_cast(8 + data[5])) { + return Switch2VendorCommand::Unsupported; + } + + switch (data[3]) { + case 0x0A: + if (data[5] != 4 || data[9] != 0 || data[10] != 0 || data[11] != 0) { + return Switch2VendorCommand::Unsupported; + } + if (data[8] == 0x05) return Switch2VendorCommand::SelectReport05; + if (data[8] == 0x09) return Switch2VendorCommand::SelectReport09; + return Switch2VendorCommand::Unsupported; + + case 0x0D: + if (data[5] == 8 && data[8] == 0x01) { + return Switch2VendorCommand::InitializeUsb; + } + return Switch2VendorCommand::Unsupported; + + default: + return Switch2VendorCommand::Unsupported; + } +} + +std::size_t switch2_build_vendor_response( + Switch2VendorCommand command, + uint8_t* output, + std::size_t capacity) { + const uint8_t* response = nullptr; + std::size_t response_length = 0; + + switch (command) { + case Switch2VendorCommand::Unsupported: + return 0; + case Switch2VendorCommand::SelectReport05: + case Switch2VendorCommand::SelectReport09: + response = kSelectReportResponse; + response_length = sizeof(kSelectReportResponse); + break; + case Switch2VendorCommand::InitializeUsb: + response = kInitializeUsbResponse; + response_length = sizeof(kInitializeUsbResponse); + break; + } + + if (output == nullptr || capacity < response_length) { + return 0; + } + std::memcpy(output, response, response_length); + return response_length; +} diff --git a/tests/firmware/test_switch2_commands.cpp b/tests/firmware/test_switch2_commands.cpp new file mode 100644 index 0000000..c9bb6d2 --- /dev/null +++ b/tests/firmware/test_switch2_commands.cpp @@ -0,0 +1,185 @@ +#include "test_support.h" + +#include +#include +#include +#include + +#include "../../switch2_commands.h" + +namespace { + +bool switch2_classifies_captured_report_selection_requests() { + // Given: captured common and Pro report-selection request vectors. + static constexpr uint8_t select_05[] = { + 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, + }; + static constexpr uint8_t select_09[] = { + 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, + 0x09, 0x00, 0x00, 0x00, + }; + + // When/Then: each captured vector selects only its represented report. + CHECK(switch2_classify_vendor_request(select_05, sizeof(select_05)) == + Switch2VendorCommand::SelectReport05); + CHECK(switch2_classify_vendor_request(select_09, sizeof(select_09)) == + Switch2VendorCommand::SelectReport09); + return true; +} + +bool switch2_builds_exact_report_selection_ack() { + // Given: the two supported report-selection classifications. + static constexpr uint8_t expected[] = { + 0x03, 0x01, 0x00, 0x0A, 0x00, 0xF8, 0x00, 0x00, + }; + std::array output{}; + + // When: either response is built. Then: both equal the captured ACK. + CHECK(switch2_build_vendor_response( + Switch2VendorCommand::SelectReport05, output.data(), output.size()) == + sizeof(expected)); + CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0); + output.fill(0); + CHECK(switch2_build_vendor_response( + Switch2VendorCommand::SelectReport09, output.data(), output.size()) == + sizeof(expected)); + CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0); + return true; +} + +bool switch2_classifies_captured_and_opaque_usb_init_requests() { + // Given: the pinned vector and another opaque host-address payload. + static constexpr uint8_t captured[] = { + 0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00, + 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + }; + static constexpr uint8_t opaque_address[] = { + 0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00, + 0x01, 0xA5, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, + }; + + // When/Then: both structurally valid requests initialize USB. + CHECK(switch2_classify_vendor_request(captured, sizeof(captured)) == + Switch2VendorCommand::InitializeUsb); + CHECK(switch2_classify_vendor_request(opaque_address, sizeof(opaque_address)) == + Switch2VendorCommand::InitializeUsb); + return true; +} + +bool switch2_builds_exact_usb_init_ack() { + // Given: the captured initialization acknowledgement. + static constexpr uint8_t expected[] = { + 0x03, 0x01, 0x00, 0x0D, 0x00, 0xF8, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, + }; + std::array output{}; + + // When: the initialization response is built. Then: every byte is exact. + CHECK(switch2_build_vendor_response( + Switch2VendorCommand::InitializeUsb, output.data(), output.size()) == + sizeof(expected)); + CHECK(std::memcmp(output.data(), expected, sizeof(expected)) == 0); + return true; +} + +bool switch2_rejects_malformed_header_and_length_fields() { + // Given: a structurally valid selection request as the mutation baseline. + static constexpr uint8_t valid[] = { + 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, + }; + + // When/Then: truncation, extension, and every fixed header violation reject. + CHECK(switch2_classify_vendor_request(nullptr, 0) == Switch2VendorCommand::Unsupported); + for (std::size_t length = 0; length < sizeof(valid); ++length) { + CHECK(switch2_classify_vendor_request(valid, length) == + Switch2VendorCommand::Unsupported); + } + std::array extended{}; + std::memcpy(extended.data(), valid, sizeof(valid)); + CHECK(switch2_classify_vendor_request(extended.data(), extended.size()) == + Switch2VendorCommand::Unsupported); + for (uint8_t offset : {0, 1, 2, 4, 6, 7}) { + std::array malformed{}; + std::memcpy(malformed.data(), valid, sizeof(valid)); + ++malformed[offset]; + CHECK(switch2_classify_vendor_request(malformed.data(), malformed.size()) == + Switch2VendorCommand::Unsupported); + } + for (uint8_t declared_length : {0x03, 0x05}) { + std::array malformed{}; + std::memcpy(malformed.data(), valid, sizeof(valid)); + malformed[5] = declared_length; + CHECK(switch2_classify_vendor_request(malformed.data(), malformed.size()) == + Switch2VendorCommand::Unsupported); + } + return true; +} + +bool switch2_rejects_unsupported_commands_and_payloads() { + // Given: valid framing mutated to unsupported subcommands and payloads. + std::array request = { + 0x03, 0x91, 0x00, 0x0A, 0x00, 0x04, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, + }; + + // When/Then: no command outside the captured subset is classified. + request[3] = 0x03; + CHECK(switch2_classify_vendor_request(request.data(), request.size()) == + Switch2VendorCommand::Unsupported); + request[3] = 0x0A; + request[8] = 0x08; + CHECK(switch2_classify_vendor_request(request.data(), request.size()) == + Switch2VendorCommand::Unsupported); + request[8] = 0x05; + request[9] = 0x01; + CHECK(switch2_classify_vendor_request(request.data(), request.size()) == + Switch2VendorCommand::Unsupported); + + std::array init = { + 0x03, 0x91, 0x00, 0x0D, 0x00, 0x08, 0x00, 0x00, + 0x00, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, + }; + CHECK(switch2_classify_vendor_request(init.data(), init.size()) == + Switch2VendorCommand::Unsupported); + return true; +} + +bool switch2_response_builder_rejects_unsupported_or_small_outputs() { + // Given: a sentinel output buffer and every unsupported capacity. + std::array output{}; + + // When/Then: unsupported, null, and short outputs produce no bytes or writes. + output.fill(0xA5); + CHECK(switch2_build_vendor_response( + Switch2VendorCommand::Unsupported, output.data(), output.size()) == 0); + for (uint8_t byte : output) CHECK(byte == 0xA5); + CHECK(switch2_build_vendor_response( + Switch2VendorCommand::InitializeUsb, nullptr, output.size()) == 0); + for (std::size_t capacity = 0; capacity < 8; ++capacity) { + output.fill(0xA5); + CHECK(switch2_build_vendor_response( + Switch2VendorCommand::SelectReport05, output.data(), capacity) == 0); + for (uint8_t byte : output) CHECK(byte == 0xA5); + } + for (std::size_t capacity = 0; capacity < 12; ++capacity) { + output.fill(0xA5); + CHECK(switch2_build_vendor_response( + Switch2VendorCommand::InitializeUsb, output.data(), capacity) == 0); + for (uint8_t byte : output) CHECK(byte == 0xA5); + } + return true; +} + +} // namespace + +void run_switch2_command_tests(TestRunner& runner) { + runner.run("Switch 2 captured report selections", switch2_classifies_captured_report_selection_requests); + runner.run("Switch 2 report selection ACK", switch2_builds_exact_report_selection_ack); + runner.run("Switch 2 captured USB initialization", switch2_classifies_captured_and_opaque_usb_init_requests); + runner.run("Switch 2 USB initialization ACK", switch2_builds_exact_usb_init_ack); + runner.run("Switch 2 malformed command framing", switch2_rejects_malformed_header_and_length_fields); + runner.run("Switch 2 unsupported commands", switch2_rejects_unsupported_commands_and_payloads); + runner.run("Switch 2 response capacity", switch2_response_builder_rejects_unsupported_or_small_outputs); +}