2d4366f454
Port the GIP protocol and authentication layers from medusalix/xone into the C++ stack: - crypto: SHA-256/HMAC (CommonCrypto), RSA PKCS#1 (Security.framework), and a self-contained P-256 ECDH validated against OpenSSL vectors - auth: v1 (RSA) and v2 (ECDH) handshake state machine - gip: header/varint/chunk handling and packet dispatch, with the kernel device model replaced by transport + client_listener interfaces Adds test_crypto, test_auth, and test_gip suites. Co-Authored-By: deepseek (deepseek/deepseek-v4-pro-0813): ported crypto, auth, and GIP
195 lines
5.9 KiB
C++
195 lines
5.9 KiB
C++
#include "unity.h"
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "gip/protocol.hpp"
|
|
|
|
void setUp() {}
|
|
void tearDown() {}
|
|
|
|
namespace {
|
|
|
|
using u8 = std::uint8_t;
|
|
using u16 = std::uint16_t;
|
|
using u32 = std::uint32_t;
|
|
|
|
class mock_transport : public xone::gip::transport {
|
|
public:
|
|
std::vector<std::vector<u8>> frames;
|
|
|
|
auto send_frame(std::span<u8 const> frame) -> int override
|
|
{
|
|
frames.emplace_back(frame.begin(), frame.end());
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
class mock_listener : public xone::gip::client_listener {
|
|
public:
|
|
int added = 0;
|
|
int removed = 0;
|
|
std::vector<std::vector<u8>> inputs;
|
|
|
|
auto on_client_added(xone::gip::client&) -> void override { ++added; }
|
|
auto on_client_removed(u8) -> void override { ++removed; }
|
|
auto on_input(xone::gip::client&, std::span<u8 const> data) -> void override
|
|
{
|
|
inputs.emplace_back(data.begin(), data.end());
|
|
}
|
|
};
|
|
|
|
// Build a controller -> host GIP frame with a simple (single-byte varint)
|
|
// header. Payloads here stay below 128 bytes so the header is even without
|
|
// the pad byte.
|
|
auto make_frame(u8 command, u8 options, u8 sequence,
|
|
std::vector<u8> const& payload) -> std::vector<u8>
|
|
{
|
|
std::vector<u8> f;
|
|
f.push_back(command);
|
|
f.push_back(options);
|
|
f.push_back(sequence);
|
|
u32 len = static_cast<u32>(payload.size());
|
|
do {
|
|
u8 b = static_cast<u8>(len & 0x7f);
|
|
len >>= 7;
|
|
if (len)
|
|
b |= 0x80;
|
|
f.push_back(b);
|
|
} while (len);
|
|
f.insert(f.end(), payload.begin(), payload.end());
|
|
return f;
|
|
}
|
|
|
|
auto make_announce() -> std::vector<u8>
|
|
{
|
|
std::vector<u8> p(28, 0);
|
|
p[8] = 0x5e; // vendor 0x045e (LE)
|
|
p[9] = 0x04;
|
|
p[10] = 0x12; // product 0x0b12
|
|
p[11] = 0x0b;
|
|
p[12] = 0x05; // fw major
|
|
p[14] = 0x0f; // fw minor
|
|
return p;
|
|
}
|
|
|
|
auto make_identify() -> std::vector<u8>
|
|
{
|
|
// identify payload = 16 unknown bytes + 8 LE16 offsets + info elements.
|
|
// The offsets are relative to the base right after the unknown header
|
|
// (so they include the 16-byte offsets array itself: first element at 16).
|
|
auto put16 = [](std::vector<u8>& v, u16 x) {
|
|
v.push_back(static_cast<u8>(x & 0xFF));
|
|
v.push_back(static_cast<u8>(x >> 8));
|
|
};
|
|
|
|
std::vector<u8> elems;
|
|
// classes at info offset 16: count=1, str_len=10, "controller"
|
|
elems.push_back(1);
|
|
put16(elems, 10);
|
|
for (char c : std::string("controller"))
|
|
elems.push_back(static_cast<u8>(c));
|
|
// firmware_versions at 29: count=1, {5, 15}
|
|
elems.push_back(1);
|
|
elems.push_back(5);
|
|
elems.push_back(0);
|
|
elems.push_back(15);
|
|
elems.push_back(0);
|
|
// capabilities_out at 34: count=1, 0x0f
|
|
elems.push_back(1);
|
|
elems.push_back(0x0f);
|
|
// capabilities_in at 36: count=1, 0x0f
|
|
elems.push_back(1);
|
|
elems.push_back(0x0f);
|
|
// interfaces at 38: count=1, 16-byte guid
|
|
elems.push_back(1);
|
|
for (int i = 0; i < 16; ++i)
|
|
elems.push_back(static_cast<u8>(i));
|
|
|
|
u16 offsets[8] = {0, 29, 0, 34, 36, 16, 38, 0};
|
|
|
|
std::vector<u8> payload(16, 0); // unknown header
|
|
for (u16 off : offsets)
|
|
put16(payload, off);
|
|
payload.insert(payload.end(), elems.begin(), elems.end());
|
|
return payload;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void test_announce_requests_identification(void)
|
|
{
|
|
mock_transport transport;
|
|
mock_listener listener;
|
|
xone::gip::adapter adap{transport, listener};
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x02, 0x21, 1, make_announce())));
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, transport.frames.size());
|
|
auto const& f = transport.frames[0];
|
|
TEST_ASSERT_EQUAL_INT(4, f.size()); // command + options + seq + len
|
|
TEST_ASSERT_EQUAL_UINT8(0x04, f[0]); // identify
|
|
TEST_ASSERT_EQUAL_UINT8(0x21, f[1]); // id 1 | internal
|
|
TEST_ASSERT_EQUAL_UINT8(0x01, f[2]); // sequence
|
|
TEST_ASSERT_EQUAL_UINT8(0x00, f[3]); // zero packet length
|
|
}
|
|
|
|
void test_identify_adds_client_and_starts_auth(void)
|
|
{
|
|
mock_transport transport;
|
|
mock_listener listener;
|
|
xone::gip::adapter adap{transport, listener};
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x04, 0x21, 2, make_identify())));
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, listener.added);
|
|
TEST_ASSERT_EQUAL_INT(1, adap.client_count());
|
|
|
|
// auth started: one AUTHENTICATE host hello frame was sent
|
|
TEST_ASSERT_EQUAL_INT(1, transport.frames.size());
|
|
auto const& f = transport.frames[0];
|
|
TEST_ASSERT_EQUAL_UINT8(0x06, f[0]); // authenticate
|
|
TEST_ASSERT_EQUAL_UINT8(0x31, f[1]); // id 1 | internal | ack
|
|
TEST_ASSERT_EQUAL_INT(62, f.size()); // 4-byte header + 58-byte hello
|
|
}
|
|
|
|
void test_input_delivers_to_listener(void)
|
|
{
|
|
mock_transport transport;
|
|
mock_listener listener;
|
|
xone::gip::adapter adap{transport, listener};
|
|
|
|
std::vector<u8> payload = {0x11, 0x22, 0x33, 0x44};
|
|
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x20, 0x01, 3, payload)));
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, listener.inputs.size());
|
|
TEST_ASSERT_EQUAL_UINT8_ARRAY(payload.data(), listener.inputs[0].data(), 4);
|
|
}
|
|
|
|
void test_status_disconnect_removes_client(void)
|
|
{
|
|
mock_transport transport;
|
|
mock_listener listener;
|
|
xone::gip::adapter adap{transport, listener};
|
|
|
|
adap.get_client(1);
|
|
TEST_ASSERT_EQUAL_INT(1, adap.client_count());
|
|
|
|
std::vector<u8> payload = {0x00, 0, 0, 0}; // status, not connected
|
|
TEST_ASSERT_EQUAL_INT(0, adap.process_buffer(make_frame(0x03, 0x21, 4, payload)));
|
|
|
|
TEST_ASSERT_EQUAL_INT(1, listener.removed);
|
|
TEST_ASSERT_EQUAL_INT(0, adap.client_count());
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_announce_requests_identification);
|
|
RUN_TEST(test_identify_adds_client_and_starts_auth);
|
|
RUN_TEST(test_input_delivers_to_listener);
|
|
RUN_TEST(test_status_disconnect_removes_client);
|
|
return UNITY_END();
|
|
}
|