mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-09-01 06:12:01 +08:00
[refactor] reorganize platform and wire sources
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user