[refactor] reorganize platform and wire sources

This commit is contained in:
dijunkun
2026-09-01 01:29:30 +08:00
parent fb0ab4f14e
commit 90184f5251
354 changed files with 2837 additions and 2456 deletions
+65
View File
@@ -0,0 +1,65 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-01
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DISPLAY_STREAM_ID_H_
#define _DISPLAY_STREAM_ID_H_
#include <cstddef>
#include <string>
namespace crossdesk {
// MiniRTC stream identifiers are wire-facing logical identifiers. Keep
// them independent from platform display handles and user-visible names, both
// of which may change or contain duplicate/Unicode text.
inline std::string MakeDisplayStreamId(size_t display_index) {
return "Display" + std::to_string(display_index + 1);
}
inline bool IsRegisteredDisplayStreamId(const std::string& stream_id,
size_t display_count) {
if (stream_id.empty()) {
return false;
}
for (size_t index = 0; index < display_count; ++index) {
if (stream_id == MakeDisplayStreamId(index)) {
return true;
}
}
return false;
}
// Resolves a backend-reported identifier to a stream registered with MiniRTC.
// Backends should report MakeDisplayStreamId(index); the fallbacks keep older
// capture plugins and hotplug transitions safe.
inline std::string ResolveDisplayStreamId(
const char* reported_id, size_t display_count, int preferred_index = -1,
const std::string& previous_id = {}) {
const std::string candidate = reported_id ? reported_id : "";
if (IsRegisteredDisplayStreamId(candidate, display_count)) {
return candidate;
}
if (preferred_index >= 0 &&
preferred_index < static_cast<int>(display_count)) {
return MakeDisplayStreamId(static_cast<size_t>(preferred_index));
}
if (IsRegisteredDisplayStreamId(previous_id, display_count)) {
return previous_id;
}
if (display_count == 1) {
return MakeDisplayStreamId(0);
}
return {};
}
} // namespace crossdesk
#endif
+70
View File
@@ -0,0 +1,70 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-01
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _FILE_TRANSFER_FORMAT_H_
#define _FILE_TRANSFER_FORMAT_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace crossdesk {
inline constexpr uint32_t kFileChunkMagic = 0x4A4E544D; // 'JNTM'
inline constexpr uint32_t kFileAckMagic = 0x4A4E5443; // 'JNTC'
#pragma pack(push, 1)
struct FileChunkHeader {
uint32_t magic;
uint32_t file_id;
uint64_t offset;
uint64_t total_size;
uint32_t chunk_size;
uint16_t name_len;
uint8_t flags;
};
struct FileTransferAck {
uint32_t magic;
uint32_t file_id;
uint64_t acked_offset;
uint64_t total_size;
uint32_t flags;
};
#pragma pack(pop)
static_assert(sizeof(FileChunkHeader) == 31,
"FileChunkHeader wire layout must remain stable");
static_assert(sizeof(FileTransferAck) == 28,
"FileTransferAck wire layout must remain stable");
struct FileChunkView {
FileChunkHeader header{};
std::string file_name;
const char* payload = nullptr;
std::size_t payload_size = 0;
};
std::vector<char> EncodeFileChunk(uint32_t file_id, uint64_t offset,
uint64_t total_size, const char* data,
uint32_t data_size,
const std::string* file_name, bool is_first,
bool is_last);
bool DecodeFileChunk(const char* data, std::size_t size,
FileChunkView* output);
std::array<char, sizeof(FileTransferAck)> EncodeFileTransferAck(
const FileTransferAck& ack);
bool DecodeFileTransferAck(const char* data, std::size_t size,
FileTransferAck* output);
} // namespace crossdesk
#endif
+139
View File
@@ -0,0 +1,139 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-01
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _REMOTE_ACTION_H_
#define _REMOTE_ACTION_H_
#include <cstddef>
#include <cstdint>
#include <string>
#include <remote_cursor_shape.h>
namespace crossdesk {
enum ControlType {
invalid = -1,
mouse = 0,
keyboard = 1,
audio_capture = 2,
host_infomation = 3,
display_id = 4,
service_status = 5,
service_command = 6,
keyboard_state = 7,
cursor_state = 8,
};
enum MouseFlag {
move = 0,
left_down,
left_up,
right_down,
right_up,
middle_down,
middle_up,
wheel_vertical,
wheel_horizontal,
};
enum KeyFlag { key_down = 0, key_up };
enum ServiceCommandFlag { send_sas = 0, lock_workstation };
struct Mouse {
float x;
float y;
int s;
MouseFlag flag;
};
struct Key {
std::size_t key_value;
uint32_t scan_code;
bool extended;
KeyFlag flag;
};
inline constexpr std::size_t kMaxKeyboardStateKeys = 32;
struct KeyboardStateKey {
std::size_t key_value;
uint32_t scan_code;
bool extended;
};
struct KeyboardState {
uint32_t seq;
std::size_t pressed_count;
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
};
struct CursorState {
uint32_t seq;
bool visible;
RemoteCursorShape shape;
bool position_valid;
float x;
float y;
// Normalized displacement from the input hotspot to the visible cursor
// anchor. This is presentation metadata and must not affect input mapping.
float visual_offset_x;
float visual_offset_y;
int display_id;
// Whether receivers should apply the position fields in this message.
// Shape-only updates set this to false so cursor appearance can remain
// responsive while position feedback to the input source is suppressed.
bool position_update;
};
struct HostInfo {
char host_name[64];
std::size_t host_name_size;
char** display_list;
std::size_t display_num;
int* left;
int* top;
int* right;
int* bottom;
};
struct ServiceStatus {
bool available;
char interactive_stage[32];
};
struct ServiceCommand {
ServiceCommandFlag flag;
};
struct RemoteAction {
ControlType type = ControlType::invalid;
union {
Mouse m;
Key k;
KeyboardState ks;
CursorState cs;
HostInfo i;
bool a;
int d;
ServiceStatus ss;
ServiceCommand c;
};
std::string to_json() const;
bool from_json(const std::string& json_string);
static std::string ToJson(const RemoteAction& action);
static bool FromJson(const std::string& json_string, RemoteAction& output);
};
// Releases the dynamically allocated display arrays held by host information.
// Other RemoteAction variants do not own memory and are left unchanged.
void FreeRemoteAction(RemoteAction& action);
} // namespace crossdesk
#endif
+51
View File
@@ -0,0 +1,51 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-01
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _REMOTE_CURSOR_SHAPE_H_
#define _REMOTE_CURSOR_SHAPE_H_
#include <cstdint>
namespace crossdesk {
// Keep these values aligned with Slint's MouseCursor enum. The wire
// intentionally carries a semantic cursor instead of a platform handle so a
// Windows, macOS or Linux host can control a different desktop platform.
enum class RemoteCursorShape : uint8_t {
default_cursor = 0,
none,
help,
pointer,
progress,
wait,
crosshair,
text,
alias,
copy,
move,
no_drop,
not_allowed,
grab,
grabbing,
col_resize,
row_resize,
n_resize,
e_resize,
s_resize,
w_resize,
ne_resize,
nw_resize,
se_resize,
sw_resize,
ew_resize,
ns_resize,
nesw_resize,
nwse_resize,
};
} // namespace crossdesk
#endif
+28
View File
@@ -0,0 +1,28 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-01
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _STREAM_NAMES_H_
#define _STREAM_NAMES_H_
#include <cstddef>
namespace crossdesk {
inline constexpr char kAudioStream[] = "control_audio";
inline constexpr char kDataStream[] = "data";
inline constexpr char kMouseStream[] = "mouse";
inline constexpr char kKeyboardStream[] = "keyboard";
inline constexpr char kControlStream[] = "control_data";
inline constexpr char kFileStream[] = "file";
inline constexpr char kFileFeedbackStream[] = "file_feedback";
inline constexpr char kClipboardStream[] = "clipboard";
inline constexpr std::size_t kFileChunkSize = 64 * 1024;
inline constexpr std::size_t kMaxClipboardBytes = 128 * 1024;
} // namespace crossdesk
#endif
+81
View File
@@ -0,0 +1,81 @@
#include <file_transfer_format.h>
#include <cstring>
#include <limits>
namespace crossdesk {
std::vector<char> EncodeFileChunk(uint32_t file_id, uint64_t offset,
uint64_t total_size, const char* data,
uint32_t data_size,
const std::string* file_name, bool is_first,
bool is_last) {
const std::size_t name_size = file_name && is_first ? file_name->size() : 0;
if (name_size > std::numeric_limits<uint16_t>::max() ||
offset > total_size || data_size > total_size - offset ||
(data_size > 0 && data == nullptr)) {
return {};
}
FileChunkHeader header{};
header.magic = kFileChunkMagic;
header.file_id = file_id;
header.offset = offset;
header.total_size = total_size;
header.chunk_size = data_size;
header.name_len = static_cast<uint16_t>(name_size);
header.flags = (is_first ? 0x01 : 0) | (is_last ? 0x02 : 0);
std::vector<char> output(sizeof(header) + name_size + data_size);
std::memcpy(output.data(), &header, sizeof(header));
std::size_t cursor = sizeof(header);
if (name_size > 0) {
std::memcpy(output.data() + cursor, file_name->data(), name_size);
cursor += name_size;
}
if (data_size > 0) {
std::memcpy(output.data() + cursor, data, data_size);
}
return output;
}
bool DecodeFileChunk(const char* data, std::size_t size,
FileChunkView* output) {
if (!data || !output || size < sizeof(FileChunkHeader)) return false;
FileChunkHeader header{};
std::memcpy(&header, data, sizeof(header));
if (header.magic != kFileChunkMagic) return false;
const std::size_t name_size = header.name_len;
const std::size_t payload_size = header.chunk_size;
if (name_size > size - sizeof(header)) return false;
const std::size_t payload_offset = sizeof(header) + name_size;
if (payload_size > size - payload_offset || header.offset > header.total_size ||
payload_size > header.total_size - header.offset) {
return false;
}
output->header = header;
output->file_name.assign(data + sizeof(header), name_size);
output->payload = data + payload_offset;
output->payload_size = payload_size;
return true;
}
std::array<char, sizeof(FileTransferAck)> EncodeFileTransferAck(
const FileTransferAck& ack) {
std::array<char, sizeof(FileTransferAck)> output{};
std::memcpy(output.data(), &ack, sizeof(ack));
return output;
}
bool DecodeFileTransferAck(const char* data, std::size_t size,
FileTransferAck* output) {
if (!data || !output || size != sizeof(FileTransferAck)) return false;
std::memcpy(output, data, sizeof(*output));
return output->magic == kFileAckMagic &&
output->acked_offset <= output->total_size;
}
} // namespace crossdesk
+300
View File
@@ -0,0 +1,300 @@
#include <remote_action.h>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <nlohmann/json.hpp>
namespace crossdesk {
namespace {
using json = nlohmann::json;
void ResetHostInfo(HostInfo& info) {
info.host_name[0] = '\0';
info.host_name_size = 0;
info.display_list = nullptr;
info.display_num = 0;
info.left = nullptr;
info.top = nullptr;
info.right = nullptr;
info.bottom = nullptr;
}
bool AllocateHostDisplays(HostInfo& info, std::size_t count) {
if (count == 0) return true;
info.display_list = static_cast<char**>(std::calloc(count, sizeof(char*)));
info.left = static_cast<int*>(std::malloc(count * sizeof(int)));
info.top = static_cast<int*>(std::malloc(count * sizeof(int)));
info.right = static_cast<int*>(std::malloc(count * sizeof(int)));
info.bottom = static_cast<int*>(std::malloc(count * sizeof(int)));
return info.display_list && info.left && info.top && info.right &&
info.bottom;
}
} // namespace
std::string RemoteAction::to_json() const { return ToJson(*this); }
bool RemoteAction::from_json(const std::string& json_string) {
RemoteAction temporary{};
if (!FromJson(json_string, temporary)) return false;
*this = temporary;
return true;
}
std::string RemoteAction::ToJson(const RemoteAction& action) {
if (action.type == ControlType::invalid) return {};
json object;
object["type"] = action.type;
switch (action.type) {
case ControlType::mouse:
object["mouse"] = {{"x", action.m.x},
{"y", action.m.y},
{"s", action.m.s},
{"flag", action.m.flag}};
break;
case ControlType::keyboard:
object["keyboard"] = {{"key_value", action.k.key_value},
{"scan_code", action.k.scan_code},
{"extended", action.k.extended},
{"flag", action.k.flag}};
break;
case ControlType::keyboard_state: {
json keys = json::array();
const std::size_t pressed_count =
std::min(action.ks.pressed_count, kMaxKeyboardStateKeys);
for (std::size_t index = 0; index < pressed_count; ++index) {
keys.push_back(
{{"key_value", action.ks.pressed_keys[index].key_value},
{"scan_code", action.ks.pressed_keys[index].scan_code},
{"extended", action.ks.pressed_keys[index].extended}});
}
object["keyboard_state"] =
{{"seq", action.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::cursor_state:
object["cursor_state"] =
{{"seq", action.cs.seq},
{"visible", action.cs.visible},
{"shape", static_cast<int>(action.cs.shape)},
{"position_valid", action.cs.position_valid},
{"x", action.cs.x},
{"y", action.cs.y},
{"visual_offset_x", action.cs.visual_offset_x},
{"visual_offset_y", action.cs.visual_offset_y},
{"display_id", action.cs.display_id},
{"position_update", action.cs.position_update}};
break;
case ControlType::audio_capture:
object["audio_capture"] = action.a;
break;
case ControlType::display_id:
object["display_id"] = action.d;
break;
case ControlType::service_status:
object["service_status"] =
{{"available", action.ss.available},
{"interactive_stage", action.ss.interactive_stage}};
break;
case ControlType::service_command:
object["service_command"] = {{"flag", action.c.flag}};
break;
case ControlType::host_infomation: {
json displays = json::array();
for (std::size_t index = 0; index < action.i.display_num; ++index) {
displays.push_back(
{{"name", action.i.display_list ? action.i.display_list[index]
: ""},
{"left", action.i.left ? action.i.left[index] : 0},
{"top", action.i.top ? action.i.top[index] : 0},
{"right", action.i.right ? action.i.right[index] : 0},
{"bottom", action.i.bottom ? action.i.bottom[index] : 0}});
}
object["host_info"] = {{"host_name", action.i.host_name},
{"display_num", action.i.display_num},
{"displays", displays}};
break;
}
case ControlType::invalid:
default:
return {};
}
return object.dump();
}
bool RemoteAction::FromJson(const std::string& json_string,
RemoteAction& output) {
bool owns_host_info = false;
try {
const json object = json::parse(json_string);
output.type = static_cast<ControlType>(object.at("type").get<int>());
switch (output.type) {
case ControlType::mouse:
output.m.x = object.at("mouse").at("x").get<float>();
output.m.y = object.at("mouse").at("y").get<float>();
output.m.s = object.at("mouse").at("s").get<int>();
output.m.flag = static_cast<MouseFlag>(
object.at("mouse").at("flag").get<int>());
break;
case ControlType::keyboard:
output.k.key_value =
object.at("keyboard").at("key_value").get<std::size_t>();
output.k.scan_code = object.at("keyboard").value("scan_code", 0u);
output.k.extended = object.at("keyboard").value("extended", false);
output.k.flag = static_cast<KeyFlag>(
object.at("keyboard").at("flag").get<int>());
break;
case ControlType::keyboard_state: {
const auto& keyboard_state_object = object.at("keyboard_state");
output.ks.seq = keyboard_state_object.value("seq", 0u);
output.ks.pressed_count = 0;
const auto keys =
keyboard_state_object.value("pressed_keys", json::array());
if (!keys.is_array()) break;
const std::size_t count =
std::min(keys.size(), kMaxKeyboardStateKeys);
for (std::size_t index = 0; index < count; ++index) {
output.ks.pressed_keys[index].key_value =
keys[index].at("key_value").get<std::size_t>();
output.ks.pressed_keys[index].scan_code =
keys[index].value("scan_code", 0u);
output.ks.pressed_keys[index].extended =
keys[index].value("extended", false);
}
output.ks.pressed_count = count;
break;
}
case ControlType::cursor_state: {
const auto& cursor_state_object = object.at("cursor_state");
const int shape = cursor_state_object.at("shape").get<int>();
if (shape < static_cast<int>(RemoteCursorShape::default_cursor) ||
shape > static_cast<int>(RemoteCursorShape::nwse_resize)) {
return false;
}
output.cs.seq = cursor_state_object.at("seq").get<uint32_t>();
output.cs.visible = cursor_state_object.at("visible").get<bool>();
output.cs.shape = static_cast<RemoteCursorShape>(shape);
output.cs.position_valid =
cursor_state_object.value("position_valid", false);
output.cs.x = cursor_state_object.value("x", 0.5f);
output.cs.y = cursor_state_object.value("y", 0.5f);
output.cs.visual_offset_x =
cursor_state_object.value("visual_offset_x", 0.0f);
output.cs.visual_offset_y =
cursor_state_object.value("visual_offset_y", 0.0f);
output.cs.display_id = cursor_state_object.value("display_id", -1);
// Cursor state messages predating position-only echo suppression
// always carried an authoritative position update.
output.cs.position_update =
cursor_state_object.value("position_update", true);
if (!std::isfinite(output.cs.x) || !std::isfinite(output.cs.y) ||
!std::isfinite(output.cs.visual_offset_x) ||
!std::isfinite(output.cs.visual_offset_y)) {
return false;
}
output.cs.x = std::clamp(output.cs.x, 0.0f, 1.0f);
output.cs.y = std::clamp(output.cs.y, 0.0f, 1.0f);
output.cs.visual_offset_x =
std::clamp(output.cs.visual_offset_x, -1.0f, 1.0f);
output.cs.visual_offset_y =
std::clamp(output.cs.visual_offset_y, -1.0f, 1.0f);
break;
}
case ControlType::audio_capture:
output.a = object.at("audio_capture").get<bool>();
break;
case ControlType::display_id:
output.d = object.at("display_id").get<int>();
break;
case ControlType::service_status: {
const auto& service_status_object = object.at("service_status");
output.ss.available = service_status_object.value("available", false);
const std::string stage = service_status_object.value(
"interactive_stage", std::string());
std::strncpy(output.ss.interactive_stage, stage.c_str(),
sizeof(output.ss.interactive_stage) - 1);
output.ss.interactive_stage[sizeof(output.ss.interactive_stage) - 1] =
'\0';
break;
}
case ControlType::service_command:
output.c.flag = static_cast<ServiceCommandFlag>(
object.at("service_command").at("flag").get<int>());
break;
case ControlType::host_infomation: {
ResetHostInfo(output.i);
owns_host_info = true;
const auto& host_info_object = object.at("host_info");
const std::string host_name =
host_info_object.at("host_name").get<std::string>();
std::strncpy(output.i.host_name, host_name.c_str(),
sizeof(output.i.host_name) - 1);
output.i.host_name[sizeof(output.i.host_name) - 1] = '\0';
output.i.host_name_size = std::strlen(output.i.host_name);
const auto& displays = host_info_object.at("displays");
if (!displays.is_array()) return false;
output.i.display_num =
host_info_object.at("display_num").get<std::size_t>();
if (output.i.display_num != displays.size()) return false;
if (!AllocateHostDisplays(output.i, output.i.display_num)) {
FreeRemoteAction(output);
return false;
}
for (std::size_t index = 0; index < output.i.display_num; ++index) {
const std::string name =
displays[index].at("name").get<std::string>();
output.i.display_list[index] =
static_cast<char*>(std::malloc(name.size() + 1));
if (!output.i.display_list[index]) {
FreeRemoteAction(output);
return false;
}
std::memcpy(output.i.display_list[index], name.c_str(),
name.size() + 1);
output.i.left[index] = displays[index].at("left").get<int>();
output.i.top[index] = displays[index].at("top").get<int>();
output.i.right[index] = displays[index].at("right").get<int>();
output.i.bottom[index] = displays[index].at("bottom").get<int>();
}
break;
}
default:
return false;
}
return true;
} catch (const std::exception& exception) {
if (owns_host_info) {
FreeRemoteAction(output);
}
std::fprintf(stderr, "Failed to parse RemoteAction JSON: %s\n",
exception.what());
return false;
}
}
void FreeRemoteAction(RemoteAction& action) {
if (action.type != ControlType::host_infomation) return;
if (action.i.display_list) {
for (std::size_t index = 0; index < action.i.display_num; ++index) {
std::free(action.i.display_list[index]);
}
}
std::free(action.i.display_list);
std::free(action.i.left);
std::free(action.i.top);
std::free(action.i.right);
std::free(action.i.bottom);
ResetHostInfo(action.i);
}
} // namespace crossdesk
@@ -0,0 +1,71 @@
#include <iostream>
#include <string>
#include <remote_action.h>
namespace {
bool ExpectEqual(const char* name, size_t actual, size_t expected) {
if (actual == expected) {
return true;
}
std::cerr << name << " mismatch\n"
<< " expected: " << expected << "\n"
<< " actual: " << actual << "\n";
return false;
}
bool ExpectTrue(const char* name, bool value) {
if (value) {
return true;
}
std::cerr << name << " expected true\n";
return false;
}
} // namespace
int main() {
bool ok = true;
ok &= ExpectEqual("mouse type", crossdesk::ControlType::mouse, 0);
ok &= ExpectEqual("keyboard type", crossdesk::ControlType::keyboard, 1);
ok &= ExpectEqual("audio_capture type", crossdesk::ControlType::audio_capture,
2);
ok &= ExpectEqual("host_infomation type",
crossdesk::ControlType::host_infomation, 3);
ok &= ExpectEqual("display_id type", crossdesk::ControlType::display_id, 4);
ok &= ExpectEqual("service_status type",
crossdesk::ControlType::service_status, 5);
ok &= ExpectEqual("service_command type",
crossdesk::ControlType::service_command, 6);
ok &= ExpectEqual("keyboard_state type",
crossdesk::ControlType::keyboard_state, 7);
ok &= ExpectEqual("cursor_state type", crossdesk::ControlType::cursor_state,
8);
crossdesk::RemoteAction action{};
action.type = crossdesk::ControlType::keyboard_state;
action.ks.seq = 42;
action.ks.pressed_count = 2;
action.ks.pressed_keys[0] = {65, 30, false};
action.ks.pressed_keys[1] = {0xA3, 29, true};
const std::string json = action.to_json();
crossdesk::RemoteAction parsed{};
ok &= ExpectTrue("parse keyboard_state", parsed.from_json(json));
ok &= ExpectEqual("parsed type", parsed.type,
crossdesk::ControlType::keyboard_state);
ok &= ExpectEqual("parsed seq", parsed.ks.seq, 42);
ok &= ExpectEqual("parsed pressed_count", parsed.ks.pressed_count, 2);
ok &= ExpectEqual("parsed key 0", parsed.ks.pressed_keys[0].key_value, 65);
ok &= ExpectEqual("parsed scan 0", parsed.ks.pressed_keys[0].scan_code, 30);
ok &= ExpectTrue("parsed extended 0", !parsed.ks.pressed_keys[0].extended);
ok &= ExpectEqual("parsed key 1", parsed.ks.pressed_keys[1].key_value, 0xA3);
ok &= ExpectEqual("parsed scan 1", parsed.ks.pressed_keys[1].scan_code, 29);
ok &= ExpectTrue("parsed extended 1", parsed.ks.pressed_keys[1].extended);
return ok ? 0 : 1;
}
+233
View File
@@ -0,0 +1,233 @@
#include <cstring>
#include <cstdint>
#include <iostream>
#include <limits>
#include <string>
#include <display_stream_id.h>
#include <file_transfer_format.h>
#include <remote_action.h>
#include <stream_names.h>
namespace {
bool Expect(bool condition, const char* message) {
if (condition) return true;
std::cerr << message << '\n';
return false;
}
} // namespace
int main() {
bool ok = true;
ok &= Expect(static_cast<int>(crossdesk::ControlType::invalid) == -1 &&
static_cast<int>(crossdesk::ControlType::mouse) == 0 &&
static_cast<int>(crossdesk::ControlType::keyboard) == 1 &&
static_cast<int>(crossdesk::ControlType::audio_capture) == 2 &&
static_cast<int>(crossdesk::ControlType::host_infomation) == 3 &&
static_cast<int>(crossdesk::ControlType::display_id) == 4 &&
static_cast<int>(crossdesk::ControlType::service_status) == 5 &&
static_cast<int>(crossdesk::ControlType::service_command) == 6 &&
static_cast<int>(crossdesk::ControlType::keyboard_state) == 7 &&
static_cast<int>(crossdesk::ControlType::cursor_state) == 8,
"ControlType wire values changed");
ok &= Expect(static_cast<int>(crossdesk::RemoteCursorShape::default_cursor) ==
0 &&
static_cast<int>(crossdesk::RemoteCursorShape::pointer) == 3 &&
static_cast<int>(crossdesk::RemoteCursorShape::nwse_resize) ==
28,
"remote cursor wire values changed");
ok &= Expect(sizeof(crossdesk::FileChunkHeader) == 31 &&
sizeof(crossdesk::FileTransferAck) == 28,
"file transfer wire structure size changed");
crossdesk::RemoteAction unset_action{};
ok &= Expect(unset_action.type == crossdesk::ControlType::invalid,
"default RemoteAction should be invalid");
ok &= Expect(unset_action.to_json().empty(),
"invalid RemoteAction should not be serialized");
crossdesk::RemoteAction parsed_invalid{};
ok &= Expect(!parsed_invalid.from_json("{\"type\":-1}"),
"invalid RemoteAction JSON should be rejected");
crossdesk::RemoteAction unknown_action{};
unknown_action.type = static_cast<crossdesk::ControlType>(999);
ok &= Expect(unknown_action.to_json().empty(),
"unknown RemoteAction type should not be serialized");
crossdesk::RemoteAction mouse{};
mouse.type = crossdesk::ControlType::mouse;
mouse.m = {0.25f, 0.75f, -1, crossdesk::MouseFlag::wheel_vertical};
crossdesk::RemoteAction parsed_mouse{};
ok &= Expect(parsed_mouse.from_json(mouse.to_json()),
"mouse JSON should decode");
ok &= Expect(parsed_mouse.type == crossdesk::ControlType::mouse &&
parsed_mouse.m.x == mouse.m.x &&
parsed_mouse.m.y == mouse.m.y &&
parsed_mouse.m.s == mouse.m.s &&
parsed_mouse.m.flag == mouse.m.flag,
"mouse JSON round trip changed fields");
crossdesk::RemoteAction cursor{};
cursor.type = crossdesk::ControlType::cursor_state;
cursor.cs = {42, true, crossdesk::RemoteCursorShape::pointer,
true, 0.25f, 0.75f, 0.001f, -0.002f, 1, true};
crossdesk::RemoteAction parsed_cursor{};
ok &= Expect(parsed_cursor.from_json(cursor.to_json()) &&
parsed_cursor.type == crossdesk::ControlType::cursor_state &&
parsed_cursor.cs.seq == cursor.cs.seq &&
parsed_cursor.cs.visible == cursor.cs.visible &&
parsed_cursor.cs.shape == cursor.cs.shape &&
parsed_cursor.cs.position_valid &&
parsed_cursor.cs.x == cursor.cs.x &&
parsed_cursor.cs.y == cursor.cs.y &&
parsed_cursor.cs.visual_offset_x ==
cursor.cs.visual_offset_x &&
parsed_cursor.cs.visual_offset_y ==
cursor.cs.visual_offset_y &&
parsed_cursor.cs.display_id == cursor.cs.display_id &&
parsed_cursor.cs.position_update,
"cursor state JSON round trip changed position fields");
crossdesk::RemoteAction legacy_cursor{};
ok &= Expect(legacy_cursor.from_json(
"{\"type\":8,\"cursor_state\":{\"seq\":1,"
"\"visible\":true,\"shape\":0}}") &&
!legacy_cursor.cs.position_valid &&
legacy_cursor.cs.position_update &&
legacy_cursor.cs.display_id == -1,
"legacy cursor state JSON should remain compatible");
crossdesk::RemoteAction shape_only_cursor{};
shape_only_cursor.type = crossdesk::ControlType::cursor_state;
shape_only_cursor.cs = cursor.cs;
shape_only_cursor.cs.position_update = false;
crossdesk::RemoteAction parsed_shape_only_cursor{};
ok &= Expect(parsed_shape_only_cursor.from_json(
shape_only_cursor.to_json()) &&
!parsed_shape_only_cursor.cs.position_update &&
parsed_shape_only_cursor.cs.shape == cursor.cs.shape,
"shape-only cursor state should preserve update semantics");
crossdesk::RemoteAction command{};
command.type = crossdesk::ControlType::service_command;
command.c.flag = crossdesk::ServiceCommandFlag::send_sas;
crossdesk::RemoteAction parsed_command{};
ok &= Expect(parsed_command.from_json(command.to_json()) &&
parsed_command.type ==
crossdesk::ControlType::service_command &&
parsed_command.c.flag ==
crossdesk::ServiceCommandFlag::send_sas,
"service command JSON round trip failed");
char display_name[] = "Built-in Display";
char* display_names[] = {display_name};
int left[] = {0};
int top[] = {0};
int right[] = {2560};
int bottom[] = {1600};
crossdesk::RemoteAction host{};
host.type = crossdesk::ControlType::host_infomation;
std::strcpy(host.i.host_name, "Test Mac");
host.i.host_name_size = std::strlen(host.i.host_name);
host.i.display_list = display_names;
host.i.display_num = 1;
host.i.left = left;
host.i.top = top;
host.i.right = right;
host.i.bottom = bottom;
crossdesk::RemoteAction parsed_host{};
ok &= Expect(parsed_host.from_json(host.to_json()) &&
parsed_host.i.display_num == 1 &&
std::string(parsed_host.i.host_name) == "Test Mac" &&
std::string(parsed_host.i.display_list[0]) ==
"Built-in Display" &&
parsed_host.i.right[0] == 2560 &&
parsed_host.i.bottom[0] == 1600,
"host information JSON round trip failed");
crossdesk::FreeRemoteAction(parsed_host);
const std::string name = "example.txt";
const std::string payload = "CrossDesk wire";
const auto encoded = crossdesk::EncodeFileChunk(
17, 0, payload.size(), payload.data(),
static_cast<uint32_t>(payload.size()), &name, true, true);
crossdesk::FileChunkView chunk;
ok &= Expect(crossdesk::DecodeFileChunk(
encoded.data(), encoded.size(), &chunk),
"file chunk should decode");
ok &= Expect(chunk.header.file_id == 17 && chunk.file_name == name &&
chunk.payload_size == payload.size() &&
std::memcmp(chunk.payload, payload.data(), payload.size()) ==
0,
"file chunk round trip changed fields");
ok &= Expect(!crossdesk::DecodeFileChunk(
encoded.data(), encoded.size() - 1, &chunk),
"truncated file chunk should be rejected");
ok &= Expect(crossdesk::EncodeFileChunk(
17, payload.size() + 1, payload.size(), nullptr, 0, nullptr,
false, false)
.empty(),
"file chunk offset beyond total size should be rejected");
ok &= Expect(crossdesk::EncodeFileChunk(
17, payload.size(), payload.size(), payload.data(), 1,
nullptr, false, false)
.empty(),
"file chunk payload beyond remaining size should be rejected");
ok &= Expect(crossdesk::EncodeFileChunk(
17, 0, 1, nullptr, 1, nullptr, false, false)
.empty(),
"non-empty file chunk with null payload should be rejected");
const std::string oversized_name(
static_cast<std::size_t>(std::numeric_limits<uint16_t>::max()) + 1,
'x');
ok &= Expect(crossdesk::EncodeFileChunk(
17, 0, 0, nullptr, 0, &oversized_name, true, true)
.empty(),
"oversized file name should be rejected");
crossdesk::FileTransferAck ack{};
ack.magic = crossdesk::kFileAckMagic;
ack.file_id = 17;
ack.acked_offset = payload.size();
ack.total_size = payload.size();
ack.flags = 1;
const auto encoded_ack = crossdesk::EncodeFileTransferAck(ack);
crossdesk::FileTransferAck decoded_ack{};
ok &= Expect(crossdesk::DecodeFileTransferAck(
encoded_ack.data(), encoded_ack.size(), &decoded_ack) &&
decoded_ack.file_id == ack.file_id &&
decoded_ack.acked_offset == ack.acked_offset &&
decoded_ack.flags == ack.flags,
"file acknowledgement round trip failed");
ok &= Expect(!crossdesk::DecodeFileTransferAck(
encoded_ack.data(), encoded_ack.size() - 1, &decoded_ack),
"short file acknowledgement should be rejected");
auto invalid_ack = encoded_ack;
crossdesk::FileTransferAck invalid_ack_value = ack;
invalid_ack_value.acked_offset = invalid_ack_value.total_size + 1;
invalid_ack =
crossdesk::EncodeFileTransferAck(invalid_ack_value);
ok &= Expect(!crossdesk::DecodeFileTransferAck(
invalid_ack.data(), invalid_ack.size(), &decoded_ack),
"file acknowledgement beyond total size should be rejected");
ok &= Expect(std::string(crossdesk::kControlStream) ==
"control_data" &&
std::string(crossdesk::kFileFeedbackStream) ==
"file_feedback",
"stream names changed");
ok &= Expect(crossdesk::MakeDisplayStreamId(0) == "Display1" &&
crossdesk::MakeDisplayStreamId(2) == "Display3" &&
crossdesk::IsRegisteredDisplayStreamId("Display2", 2) &&
!crossdesk::IsRegisteredDisplayStreamId("Display3", 2),
"display stream identifiers changed");
ok &= Expect(crossdesk::ResolveDisplayStreamId(nullptr, 1) == "Display1" &&
crossdesk::ResolveDisplayStreamId("invalid", 3, 1) ==
"Display2",
"display stream fallback changed");
return ok ? 0 : 1;
}