20 Commits

Author SHA1 Message Date
3a5a7a17a3 idk 2026-04-13 08:04:43 -03:00
0486f2a285 Added initial COT Sensor payloads to support UAV Tools integration for Arma Drones 2026-04-13 08:03:31 -03:00
753dcab26e Added hellanmaaw winter support 2026-03-31 07:23:48 -03:00
2f53488ba8 Added video url prop to 3den editor/zeus, allowing to parse __video prop to cots 2026-03-31 07:21:29 -03:00
323339e679 Removed video addon, too simple for a specific addon 2026-03-31 07:20:19 -03:00
3f14a75e81 Added video url parser to CoT types 2026-03-31 07:19:39 -03:00
469a54c141 Added Hellanmma map support 2026-03-31 07:18:23 -03:00
2ee9030c00 Updated media folder 2026-03-26 14:45:08 -03:00
5b29a40990 Improved mTLS description on readme 2026-03-26 03:47:54 -03:00
708fe5e670 Fixed CoT queue during armatak connection to the TAK Server, running soft as butter 2026-03-26 03:45:05 -03:00
e32aadda4e Splitted Connection Module 2026-03-26 01:05:54 -03:00
c35b7f0268 Updated project readme file 2026-03-24 16:56:26 -03:00
876cf900c3 Changed dialogs and module UI to get mTLS needed params 2026-03-24 16:56:19 -03:00
778ac0ac54 Added the mTLS connection calls to zeus and 3den modules 2026-03-24 16:55:53 -03:00
b816144fb0 Added transport layer and configured extension commands to call mTLS socket connection 2026-03-24 16:55:36 -03:00
61ba9f6d63 Added connector and enrollment for mTLS client certificate auto enrollment on game sessions, will MOCK a official tak client behavior when authenticating 2026-03-24 16:55:05 -03:00
f88c02a7aa formatted some rust files for linting porpuses 2026-03-24 16:44:22 -03:00
5ffc08e6f1 Readded reqwest dependency to cargo toml, will be used for TAK Server API interaction on authencated tak server connections 2026-03-24 16:41:38 -03:00
9392380c78 Added hemtt private key to git ignore 2026-03-24 16:40:58 -03:00
a18343b81d Commented video module 2026-03-24 14:03:28 -03:00
134 changed files with 12622 additions and 843 deletions

3
.gitignore vendored
View File

@@ -3,6 +3,7 @@
hemtt hemtt
hemtt.exe hemtt.exe
*.biprivatekey *.biprivatekey
.hemttprivatekey
source/ source/
.vscode .vscode
releases/ releases/
@@ -87,4 +88,4 @@ target/
.cxx .cxx
local.properties local.properties
*.apk *.apk

1073
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,10 @@ chrono = "0.4.39"
lazy_static = "1.5.0" lazy_static = "1.5.0"
log = "0.4.22" log = "0.4.22"
log4rs = "1.3.0" log4rs = "1.3.0"
reqwest = { version = "0.12.15", default-features = false, features = ["blocking", "json", "rustls-tls"] }
rcgen = { version = "0.13.2", default-features = false, features = ["crypto", "pem", "aws_lc_rs"] }
rustls = "0.23.23"
rustls-pemfile = "2.2.0"
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
[dependencies.uuid] [dependencies.uuid]

View File

@@ -4,6 +4,10 @@
ARMATAK is a server side Arma 3 addons for streaming unit positions to TAK Clients in sessions on real locations maps. It can be runned both as a clientside mod or a serverside mod, when runned serverside, it will create a TCP Socket connection between Arma 3 and the TAK Server, sending the game session information into it. When used clientside, Arma 3 will host a websocket server that you can connect to your phone and mock the phone's location to the player's in game location. ARMATAK is a server side Arma 3 addons for streaming unit positions to TAK Clients in sessions on real locations maps. It can be runned both as a clientside mod or a serverside mod, when runned serverside, it will create a TCP Socket connection between Arma 3 and the TAK Server, sending the game session information into it. When used clientside, Arma 3 will host a websocket server that you can connect to your phone and mock the phone's location to the player's in game location.
The server-side CoT router supports two transports:
- Plain TCP, for legacy TAK ingress.
- Mutual TLS, using the TAK Server authentication API, so the Arma session can publish as an authenticated TAK device on port `8089`.
## Get in Touch ## Get in Touch
[Join the Discord Server for ARMATAK!](https://discord.gg/svK64PCycU) [Join the Discord Server for ARMATAK!](https://discord.gg/svK64PCycU)

View File

@@ -107,6 +107,12 @@ switch (toLower worldName) do {
case "rut_mandol": { case "rut_mandol": {
_realLocation = _position call armatak_fnc_convert_to_rut_mandol; _realLocation = _position call armatak_fnc_convert_to_rut_mandol;
}; };
case "hellanmaa": {
_realLocation = _position call armatak_fnc_convert_to_hellanmaa;
};
case "hellanmaaw": {
_realLocation = _position call armatak_fnc_convert_to_hellanmaa;
};
default { default {
_warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", "Unsupported Map"]; _warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", "Unsupported Map"];
[[_warning, 1.5]] call CBA_fnc_notify; [[_warning, 1.5]] call CBA_fnc_notify;

View File

@@ -78,6 +78,16 @@ class Cfg3den {
condition = "objectVehicle"; condition = "objectVehicle";
typeName = "STRING"; typeName = "STRING";
}; };
class armatak_attribute_video_url {
displayName = "Video URL (RTSP)";
tooltip = "RTSP stream URL for UAS Tool integration. When set, the drone will appear in the ATAK UAS Tool with FOV cone and video feed. Format: rtsp://address:port/path (e.g. rtsp://192.168.1.10:8554/live/drone1). Leave empty to disable UAS Tool integration for this entity.";
property = "armatak_attribute_video_url";
control = "Edit";
expression = "_this setVariable ['armatak_attribute_video_url',_value]";
defaultValue = "''";
condition = "objectVehicle";
typeName = "STRING";
};
}; };
}; };
}; };

View File

@@ -19,6 +19,12 @@ class CfgFunctions {
class send_marker_cot { class send_marker_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_marker_cot.sqf"; file = "\armatak\armatak\addons\main\functions\api\fn_send_marker_cot.sqf";
}; };
class send_uas_video_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_video_cot.sqf";
};
class send_uas_sensor_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_sensor_cot.sqf";
};
class stop_tcp_socket { class stop_tcp_socket {
file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf"; file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf";
}; };

View File

@@ -35,7 +35,12 @@ addMissionEventHandler ["ExtensionCallback", {
[_function, "success", _name] call FUNC(notify); [_function, "success", _name] call FUNC(notify);
}; };
case "TCP SOCKET ERROR": { case "TCP SOCKET ERROR": {
[_function, "error", _name] call FUNC(notify); _message = _function;
if (_data isNotEqualTo "") then {
_message = format ["%1: %2", _function, _data];
};
[_message, "error", _name] call FUNC(notify);
}; };
case "VIDEO": { case "VIDEO": {
[_function, "success", _name] call FUNC(notify); [_function, "success", _name] call FUNC(notify);

View File

@@ -32,3 +32,6 @@ if (!isNil "_pre_defined_role") then {
}; };
_cot = [_drone, _atak_role, _atak_callsign] call armatak_fnc_send_marker_cot; _cot = [_drone, _atak_role, _atak_callsign] call armatak_fnc_send_marker_cot;
[_drone] call armatak_fnc_send_uas_video_cot;
[_drone] call armatak_fnc_send_uas_sensor_cot;

View File

@@ -4,10 +4,11 @@
params ["_unit", "_type", "_callsign"]; params ["_unit", "_type", "_callsign"];
_unit_position = _unit call armatak_client_fnc_extractClientPosition; _unit_position = _unit call armatak_client_fnc_extractClientPosition;
_video_url = [_unit] call armatak_fnc_extract_marker_video_url;
_uuid = _unit call armatak_fnc_extract_uuid;
_uuid = _unit call armatak_fnc_extract_uuid;
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6];
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6, _video_url];
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];

View File

@@ -0,0 +1,55 @@
// function name: armatak_fnc_send_uas_sensor_cot
// function author: Valmo / ArmaTAK contributors
// function description:
// Sends a b-m-p-s-p-loc CoT event every router tick (1 s) for a drone.
// This is the "sensor position" event consumed by the ATAK UAS Tool to:
// - Draw the FOV cone on the moving map.
// - Compute four-corners for AR marker overlay on the video feed.
// - Show the SPoI (Sensor Point of Interest) crosshair.
//
// The event references the drone's b-i-v video endpoint via the drone UUID,
// so armatak_fnc_send_uas_video_cot must also be called for the same drone.
//
// Exits silently when "armatak_attribute_video_url" is not set, which keeps
// the behavior identical to the old fn_send_drone_cot for drones without a
// configured video stream.
//
// Arguments:
// 0: _drone <OBJECT> The drone object.
//
// Return value: none
params ["_drone"];
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""];
if (_video_url == "") exitWith {};
private _uuid = _drone call armatak_fnc_extract_uuid;
private _sensor_uid = _uuid + "-sensor";
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
private _pos = (getPos _drone) call armatak_client_fnc_convertClientLocation;
private _lat = _pos select 0;
private _lon = _pos select 1;
private _hae = _pos select 2;
private _azimuth = parseNumber ((getDir _drone) toFixed 0);
private _allTurrets = [_drone, false] call BIS_fnc_allTurrets;
if (count _allTurrets > 0) then {
private _firstTurretPath = _allTurrets select 0;
private _turretWeapons = _drone weaponsTurret _firstTurretPath;
if (_turretWeapons isNotEqualTo []) then {
private _tDir = _drone weaponDirection (_turretWeapons select 0);
if (!((_tDir select 0) == 0 && (_tDir select 1) == 0)) then {
_azimuth = round (((_tDir select 0) atan2 (_tDir select 1) + 360) mod 360);
};
};
};
private _fov = _drone getVariable ["armatak_uas_fov", 60];
private _range = round (((getPosATL _drone) select 2) max 1);
private _payload = [_sensor_uid, _uuid, _callsign, _lat, _lon, _hae, _azimuth, _fov, _range];
"armatak" callExtension ["tcp_socket:cot:uas_sensor", [_payload]];

View File

@@ -0,0 +1,30 @@
// function name: armatak_fnc_send_uas_video_cot
// function author: Valmo / ArmaTAK contributors
// function description:
// Sends a b-i-v CoT event that declares the RTSP video endpoint for a drone.
// The ATAK UAS Tool picks this up and shows the drone in its UAS list with
// the associated video feed available for playback.
//
// The drone entity MUST have the variable "armatak_attribute_video_url" set
// to a valid RTSP URL, e.g.:
// _drone setVariable ["armatak_attribute_video_url", "rtsp://192.168.1.10:8554/live/drone1"];
// or via the 3DEN attribute "Video URL (RTSP)" in the ARMA Team Awareness Kit
// attribute category.
//
// If the variable is absent or empty the function exits silently.
//
// Arguments:
// 0: _drone <OBJECT> The drone object.
//
// Return value: none
params ["_drone"];
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""];
if (_video_url == "") exitWith {};
private _uuid = _drone call armatak_fnc_extract_uuid;
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
private _payload = [_uuid, _callsign, _video_url];
"armatak" callExtension ["tcp_socket:cot:uas_video", [_payload]];

View File

@@ -0,0 +1,13 @@
// function name: armatak_fnc_extract_marker_video_url
// function author: Codex
// function description: Gets the marker video URL configured in 3DEN for a vehicle
params ["_unit"];
private _videoUrl = _unit getVariable ["armatak_attribute_marker_video_url", ""];
if (isNil "_videoUrl") exitWith {
""
};
_videoUrl

View File

@@ -0,0 +1,30 @@
params ["_longitudeInGame", "_latitudeInGame", "_altitude"];
private _mapWidth = 8192;
private _mapHeight = 8192;
// SW corner (used as origin)
private _SW_lat = 63.005389;
private _SW_lon = 22.638957;
// SE corner
private _SE_lat = 63.010092;
private _SE_lon = 22.800107;
// NW corner
private _NW_lat = 63.078713;
private _NW_lon = 22.628542;
private _edgeSE_lat = _SE_lat - _SW_lat;
private _edgeSE_lon = _SE_lon - _SW_lon;
private _edgeNW_lat = _NW_lat - _SW_lat;
private _edgeNW_lon = _NW_lon - _SW_lon;
private _fx = _longitudeInGame / _mapWidth;
private _fy = _latitudeInGame / _mapHeight;
private _realLat = _SW_lat + (_fx * _edgeSE_lat) + (_fy * _edgeNW_lat);
private _realLon = _SW_lon + (_fx * _edgeSE_lon) + (_fy * _edgeNW_lon);
[_realLat, _realLon, _altitude]

View File

@@ -1,34 +1,28 @@
class CfgVehicles { class CfgVehicles {
class Logic; class Logic;
class Module_F : Logic class Module_F : Logic {
{ class AttributesBase {
class AttributesBase
{
class Edit; class Edit;
class ModuleDescription; class ModuleDescription;
}; };
class ModuleDescription; class ModuleDescription;
}; };
class GVAR(moduleBase): Module_F { class GVAR(moduleBase): Module_F {
author = PROJECT_AUTHOR; author = PROJECT_AUTHOR;
category = QEGVAR(main,moduleCategory); category = QEGVAR(main,moduleCategory);
function = QUOTE({}); function = QUOTE({});
functionPriority = 1; functionPriority = 1;
isGlobal = 1; isGlobal = 1;
isTriggerActivated = 0; isTriggerActivated = 0;
scope = 1; scope = 1;
scopeCurator = 2; scopeCurator = 2;
}; };
class GVAR(coreModule): GVAR(moduleBase) { class GVAR(connectionModuleBase): GVAR(moduleBase) {
scope = 2;
scopeCurator = 0; scopeCurator = 0;
displayname = "CoT Router";
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa"; icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
category = QEGVAR(main,moduleCategory); category = QEGVAR(main,moduleCategory);
function = QFUNC(3denCoreModuleConfig);
functionPriority = 1; functionPriority = 1;
isGlobal = 0; isGlobal = 0;
isTriggerActivated = 1; isTriggerActivated = 1;
@@ -39,19 +33,25 @@ class CfgVehicles {
canSetArea = 0; canSetArea = 0;
canSetAreaShape = 0; canSetAreaShape = 0;
canSetAreaHeight = 0; canSetAreaHeight = 0;
};
class GVAR(tcpModule): GVAR(connectionModuleBase) {
scope = 2;
displayName = "CoT Router (TCP)";
function = QFUNC(3denTcpModuleConfig);
class Attributes: AttributesBase { class Attributes: AttributesBase {
class GVAR(moduleInstanceAddress): Edit { class GVAR(moduleInstanceAddress): Edit {
property = QGVAR(moduleInstanceAddress); property = QGVAR(moduleInstanceAddress);
displayname = "TAK Server Address"; displayName = "TAK Server Address";
tooltip = "TAK Server Instance Address"; tooltip = "Hostname or IP address for the TAK or IronTAK server.";
typeName = "STRING"; typeName = "STRING";
defaultValue = "localhost"; defaultValue = "'localhost'";
}; };
class GVAR(moduleInstancePort): Edit { class GVAR(moduleInstancePort): Edit {
property = QGVAR(moduleInstancePort); property = QGVAR(moduleInstancePort);
displayname = "TAK Server TCP Port"; displayName = "TAK Server TCP Port";
tooltip = "TAK Server instance Port for TCP connection"; tooltip = "Port for the unauthenticated TCP socket.";
typeName = "NUMBER"; typeName = "NUMBER";
defaultValue = "8088"; defaultValue = "8088";
}; };
@@ -59,24 +59,75 @@ class CfgVehicles {
}; };
class ModuleDescription: ModuleDescription { class ModuleDescription: ModuleDescription {
description = "Generate the initial ARMATAK configuration, syncronizing all players to the TAK server instance"; description = "Connect ArmaTAK to a TAK server over plain TCP.";
sync[] = {"LocationArea_F"}; sync[] = {"LocationArea_F"};
}; };
}; };
class GVAR(coreModuleCurator): GVAR(coreModule) { class GVAR(enrollModule): GVAR(connectionModuleBase) {
scope = 2;
displayName = "CoT Router (Authenticated)";
function = QFUNC(3denEnrollModuleConfig);
class Attributes: AttributesBase {
class GVAR(moduleInstanceAddress): Edit {
property = QGVAR(moduleInstanceAddress);
displayname = "TAK Server Address";
tooltip = "Hostname or IP address used for enrollment and the final TLS connection.";
typeName = "STRING";
defaultValue = "'localhost'";
};
class GVAR(moduleEnrollmentPort): Edit {
property = QGVAR(moduleEnrollmentPort);
displayName = "Enrollment HTTPS Port";
tooltip = "Port used for GET /Marti/api/tls/config and POST /Marti/api/tls/signClient/v2.";
typeName = "NUMBER";
defaultValue = "8446";
};
class GVAR(moduleEnrollmentUsername): Edit {
property = QGVAR(moduleEnrollmentUsername);
displayName = "Enrollment Username";
tooltip = "Username used in Basic Auth for client certificate enrollment.";
typeName = "STRING";
defaultValue = "''";
};
class GVAR(moduleEnrollmentPassword): Edit {
property = QGVAR(moduleEnrollmentPassword);
displayName = "Enrollment Password";
tooltip = "Password used in Basic Auth for client certificate enrollment.";
typeName = "STRING";
defaultValue = "''";
};
class ModuleDescription: ModuleDescription {};
};
class ModuleDescription: ModuleDescription {
description = "Enroll a client certificate and connect ArmaTAK over mTLS.";
sync[] = {"LocationArea_F"};
};
};
class GVAR(tcpModuleCurator): GVAR(tcpModule) {
scope = 1; scope = 1;
scopeCurator = 2; scopeCurator = 2;
function = ""; function = "";
displayName = "CoT Router (Zeus)"; displayName = "CoT Router (TCP, Zeus)";
curatorInfoType = "armatak_zeus_core_module_dialog"; curatorInfoType = "armatak_zeus_tcp_module_dialog";
};
class GVAR(enrollModuleCurator): GVAR(enrollModule) {
scope = 1;
scopeCurator = 2;
function = "";
displayName = "CoT Router (Authenticated, Zeus)";
curatorInfoType = "armatak_zeus_enroll_module_dialog";
}; };
class GVAR(markEntity): GVAR(moduleBase) { class GVAR(markEntity): GVAR(moduleBase) {
curatorCanAttach = 1; curatorCanAttach = 1;
category = QEGVAR(main,moduleCategory); category = QEGVAR(main,moduleCategory);
displayname = "Mark Entity"; displayname = "Mark Entity";
function = QFUNC(routerEntityAdd); function = QFUNC(routerEntityAdd);
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa"; icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
}; };
}; };

View File

@@ -1,4 +1,7 @@
PREP(3denCoreModuleConfig); PREP(3denEnrollModuleConfig);
PREP(3denTcpModuleConfig);
PREP(routerEntityAdd); PREP(routerEntityAdd);
PREP(routerEntityRemove); PREP(routerEntityRemove);
PREP(ZeusCoreModuleConfig); PREP(startCotRouter);
PREP(ZeusEnrollModuleConfig);
PREP(ZeusTcpModuleConfig);

View File

@@ -4,8 +4,10 @@ class CfgPatches {
class ADDON { class ADDON {
name = COMPONENT_NAME; name = COMPONENT_NAME;
units[] = { units[] = {
QGVAR(coreModule), QGVAR(tcpModule),
QGVAR(coreModuleCurator), QGVAR(tcpModuleCurator),
QGVAR(enrollModule),
QGVAR(enrollModuleCurator),
QGVAR(markEntity) QGVAR(markEntity)
}; };
weapons[] = {}; weapons[] = {};

View File

@@ -3,69 +3,172 @@ class RscBackground;
class RscButton; class RscButton;
class RscEdit; class RscEdit;
class armatak_zeus_core_module_dialog { class armatak_zeus_tcp_module_dialog {
idd = 999991; idd = 999991;
movingEnable = 0; movingEnable = 0;
class ControlsBackground { class ControlsBackground {
class armatak_gui_module_zeus_core_dialog_main_frame: RscBackground { class main_frame: RscBackground {
idc = 1800; idc = 1800;
x = "0.386562 * safezoneW + safezoneX"; x = "0.386562 * safezoneW + safezoneX";
y = "0.401 * safezoneH + safezoneY"; y = "0.29 * safezoneH + safezoneY";
w = "0.216563 * safezoneW"; w = "0.216563 * safezoneW";
h = "0.242 * safezoneH"; h = "0.32 * safezoneH";
colorBackground[]={0,0,0,0.45}; colorBackground[] = {0,0,0,0.45};
}; };
}; };
class Controls { class Controls {
class armatak_gui_module_zeus_core_dialog_address_edit: RscEdit { class address_text: RscText {
idc = 14000;
text = "localhost";
x = "0.391719 * safezoneW + safezoneX";
y = "0.445 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[]={0,0,0,0.5};
};
class armatak_gui_module_zeus_core_dialog_address_port_edit: RscEdit {
idc = 14001;
text = "8088";
x = "0.391719 * safezoneW + safezoneX";
y = "0.522 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[]={0,0,0,0.5};
};
class armatak_gui_module_zeus_core_dialog_address_text: RscText {
idc = 1000; idc = 1000;
text = "TAK Server Address"; text = "TAK Server Address";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.412 * safezoneH + safezoneY"; y = "0.332 * safezoneH + safezoneY";
w = "0.20625 * safezoneW"; w = "0.20625 * safezoneW";
h = "0.033 * safezoneH"; h = "0.033 * safezoneH";
}; };
class armatak_gui_module_zeus_core_dialog_address_port_text: RscText { class address_edit: RscEdit {
idc = 14000;
text = "localhost";
x = "0.391719 * safezoneW + safezoneX";
y = "0.365 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class port_text: RscText {
idc = 1001; idc = 1001;
text = "TAK Server Port"; text = "TAK Server Port";
x = "0.391719 * safezoneW + safezoneX"; x = "0.391719 * safezoneW + safezoneX";
y = "0.489 * safezoneH + safezoneY"; y = "0.425 * safezoneH + safezoneY";
w = "0.20625 * safezoneW"; w = "0.20625 * safezoneW";
h = "0.033 * safezoneH"; h = "0.033 * safezoneH";
}; };
class armatak_gui_module_zeus_core_dialog_address_button_cancel: RscButton { class port_edit: RscEdit {
idc = 14001;
text = "8088";
x = "0.391719 * safezoneW + safezoneX";
y = "0.458 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class button_cancel: RscButton {
idc = 1601; idc = 1601;
text = "Cancel"; text = "Cancel";
action = "closeDialog 2;"; action = "closeDialog 2;";
x = "0.551563 * safezoneW + safezoneX"; x = "0.551563 * safezoneW + safezoneX";
y = "0.577 * safezoneH + safezoneY"; y = "0.535 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW"; w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH"; h = "0.055 * safezoneH";
}; };
class armatak_gui_module_zeus_core_dialog_address_button_ok: RscButton { class button_ok: RscButton {
idc = 1600; idc = 1600;
text = "Ok"; text = "Ok";
action = QUOTE(call FUNC(zeusCoreModuleConfig)); action = QUOTE(call FUNC(ZeusTcpModuleConfig));
x = "0.5 * safezoneW + safezoneX"; x = "0.5 * safezoneW + safezoneX";
y = "0.577 * safezoneH + safezoneY"; y = "0.535 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH";
};
};
};
class armatak_zeus_enroll_module_dialog {
idd = 999992;
movingEnable = 0;
class ControlsBackground {
class main_frame: RscBackground {
idc = 1810;
x = "0.386562 * safezoneW + safezoneX";
y = "0.2 * safezoneH + safezoneY";
w = "0.216563 * safezoneW";
h = "0.52 * safezoneH";
colorBackground[] = {0,0,0,0.45};
};
};
class Controls {
class address_text: RscText {
idc = 1010;
text = "TAK Server Address";
x = "0.391719 * safezoneW + safezoneX";
y = "0.242 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class address_edit: RscEdit {
idc = 14100;
text = "localhost";
x = "0.391719 * safezoneW + safezoneX";
y = "0.275 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class enroll_port_text: RscText {
idc = 1011;
text = "Enrollment HTTPS Port";
x = "0.391719 * safezoneW + safezoneX";
y = "0.335 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class enroll_port_edit: RscEdit {
idc = 14101;
text = "8446";
x = "0.391719 * safezoneW + safezoneX";
y = "0.368 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class username_text: RscText {
idc = 1012;
text = "Enrollment Username";
x = "0.391719 * safezoneW + safezoneX";
y = "0.428 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class username_edit: RscEdit {
idc = 14102;
text = "";
x = "0.391719 * safezoneW + safezoneX";
y = "0.461 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class password_text: RscText {
idc = 1013;
text = "Enrollment Password";
x = "0.391719 * safezoneW + safezoneX";
y = "0.521 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class password_edit: RscEdit {
idc = 14103;
text = "";
x = "0.391719 * safezoneW + safezoneX";
y = "0.554 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class button_cancel: RscButton {
idc = 1611;
text = "Cancel";
action = "closeDialog 2;";
x = "0.551563 * safezoneW + safezoneX";
y = "0.645 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH";
};
class button_ok: RscButton {
idc = 1610;
text = "Ok";
action = QUOTE(call FUNC(ZeusEnrollModuleConfig));
x = "0.5 * safezoneW + safezoneX";
y = "0.645 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW"; w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH"; h = "0.055 * safezoneH";
}; };

View File

@@ -1,64 +0,0 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = _logic getVariable QGVAR(moduleInstanceAddress);
_tak_server_instance_port = _logic getVariable QGVAR(moduleInstancePort);
_tak_server_fulladdress = _tak_server_instance_address + ":" + (str _tak_server_instance_port);
missionNamespace setVariable ["armatak_server_instance", _tak_server_fulladdress];
missionNamespace setVariable ["armatak_tcp_socket_is_running", true];
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
_syncUnits = synchronizedObjects _logic;
missionNamespace setVariable ["armatak_server_syncedUnits", _syncUnits];
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[{
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
{
_objectType = _x call BIS_fnc_objectType;
switch (true) do {
case ((_objectType select 0) == "Soldier"): {
_callsign = [_x] call armatak_fnc_extract_unit_callsign;
_group_name = [group _x] call armatak_fnc_extract_group_color;
_group_role = [_x] call armatak_fnc_extract_group_role;
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
case ((_objectType select 0) == "Vehicle"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot;
_x call armatak_fnc_extract_sensor_data;
};
case ((_objectType select 0) == "VehicleAutonomous"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
_x call armatak_fnc_extract_sensor_data;
};
};
} forEach GVAR(syncedUnits);
}, 1, []] call CBA_fnc_addPerFrameHandler;
};
true;

View File

@@ -0,0 +1,37 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
};
["Connecting to authenticated TAK socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = _logic getVariable [QGVAR(moduleInstanceAddress), "localhost"];
_tak_server_enrollment_port = _logic getVariable [QGVAR(moduleEnrollmentPort), 8446];
_tak_server_enrollment_username = _logic getVariable [QGVAR(moduleEnrollmentUsername), ""];
_tak_server_enrollment_password = _logic getVariable [QGVAR(moduleEnrollmentPassword), ""];
"armatak" callExtension [
"tcp_socket:start_enroll_mtls",
[
_tak_server_instance_address,
_tak_server_instance_address,
str _tak_server_enrollment_port,
_tak_server_enrollment_username,
_tak_server_enrollment_password,
""
]
];
missionNamespace setVariable ["armatak_server_syncedUnits", synchronizedObjects _logic];
_tak_server_instance_address call FUNC(startCotRouter);
};
true

View File

@@ -0,0 +1,26 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
};
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = _logic getVariable [QGVAR(moduleInstanceAddress), "localhost"];
_tak_server_instance_port = _logic getVariable [QGVAR(moduleInstancePort), 8088];
_tak_server_fulladdress = _tak_server_instance_address + ":" + (str _tak_server_instance_port);
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
missionNamespace setVariable ["armatak_server_syncedUnits", synchronizedObjects _logic];
_tak_server_fulladdress call FUNC(startCotRouter);
};
true

View File

@@ -1,67 +0,0 @@
#include "..\script_component.hpp"
params ["_logic"];
_socket_is_running = missionNamespace getVariable ["armatak_tcp_socket_is_running", false];
if (_socket_is_running) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
closeDialog 1;
};
disableSerialization;
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = ctrlText 14000;
_tak_server_instance_port = ctrlText 14001;
_tak_server_fulladdress = ((_tak_server_instance_address) + ":" + (_tak_server_instance_port));
missionNamespace setVariable ["armatak_server_instance", _tak_server_fulladdress];
missionNamespace setVariable ["armatak_tcp_socket_is_running", true];
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
_syncUnits = [];
missionNamespace setVariable ["armatak_server_syncedUnits", _syncUnits];
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[{
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
{
_objectType = _x call BIS_fnc_objectType;
switch (true) do {
case ((_objectType select 0) == "Soldier"): {
_callsign = [_x] call armatak_fnc_extract_unit_callsign;
_group_name = [group _x] call armatak_fnc_extract_group_color;
_group_role = [_x] call armatak_fnc_extract_group_role;
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
case ((_objectType select 0) == "Vehicle"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot;
_x call armatak_fnc_extract_sensor_data;
};
case ((_objectType select 0) == "VehicleAutonomous"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
_x call armatak_fnc_extract_sensor_data;
};
};
} forEach GVAR(syncedUnits);
}, 1, []] call CBA_fnc_addPerFrameHandler;
deleteVehicle _logic;
closeDialog 1;

View File

@@ -0,0 +1,33 @@
#include "..\script_component.hpp"
params ["_logic"];
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
closeDialog 1;
};
disableSerialization;
["Connecting to authenticated TAK socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = ctrlText 14100;
_tak_server_enrollment_port = ctrlText 14101;
_tak_server_enrollment_username = ctrlText 14102;
_tak_server_enrollment_password = ctrlText 14103;
"armatak" callExtension [
"tcp_socket:start_enroll_mtls",
[
_tak_server_instance_address,
_tak_server_instance_address,
_tak_server_enrollment_port,
_tak_server_enrollment_username,
_tak_server_enrollment_password,
""
]
];
_tak_server_instance_address call FUNC(startCotRouter);
deleteVehicle _logic;
closeDialog 1;

View File

@@ -0,0 +1,22 @@
#include "..\script_component.hpp"
params ["_logic"];
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
closeDialog 1;
};
disableSerialization;
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = ctrlText 14000;
_tak_server_instance_port = ctrlText 14001;
_tak_server_fulladdress = _tak_server_instance_address + ":" + _tak_server_instance_port;
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
_tak_server_fulladdress call FUNC(startCotRouter);
deleteVehicle _logic;
closeDialog 1;

View File

@@ -0,0 +1,47 @@
#include "..\script_component.hpp"
params [["_server_instance", "", [""]]];
missionNamespace setVariable ["armatak_server_instance", _server_instance];
missionNamespace setVariable ["armatak_tcp_socket_is_running", true];
if (isNil { missionNamespace getVariable "armatak_server_syncedUnits" }) then {
missionNamespace setVariable ["armatak_server_syncedUnits", []];
};
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[{
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
{
_objectType = _x call BIS_fnc_objectType;
switch (true) do {
case ((_objectType select 0) == "Soldier"): {
_callsign = [_x] call armatak_fnc_extract_unit_callsign;
_group_name = [group _x] call armatak_fnc_extract_group_color;
_group_role = [_x] call armatak_fnc_extract_group_role;
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
case ((_objectType select 0) == "Vehicle"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot;
_x call armatak_fnc_extract_sensor_data;
};
case ((_objectType select 0) == "VehicleAutonomous"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
_x call armatak_fnc_extract_sensor_data;
};
};
} forEach GVAR(syncedUnits);
}, 1, []] call CBA_fnc_addPerFrameHandler;
true

View File

@@ -1 +0,0 @@
armatak\armatak\addons\video

View File

@@ -1,11 +0,0 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
};
};
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
};
};

View File

@@ -1,73 +0,0 @@
class CfgVehicles {
class Logic;
class Module_F : Logic
{
class AttributesBase
{
class Default;
class Edit;
class Combo;
class Checkbox;
class CheckboxNumber;
class ModuleDescription;
class Units;
};
class ModuleDescription
{
class AnyBrain;
};
};
class EGVAR(server,moduleBase);
class GVAR(videoModule): EGVAR(server,moduleBase) {
scope = 2;
scopeCurator = 0;
displayname = "Video Streaming Handler";
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
category = QEGVAR(main,moduleCategory);
function = QFUNC(videoParser);
functionPriority = 1;
isGlobal = 0;
isTriggerActivated = 1;
isDisposable = 1;
is3den = 0;
curatorCanAttach = 0;
curatorInfoType = "RscDisplayAttributeModuleNuke";
canSetArea = 0;
canSetAreaShape = 0;
canSetAreaHeight = 0;
/*
class Attributes: AttributesBase {
class GVAR(instanceAddress): Edit {
property = QGVAR(instanceAddress);
displayname = "MediaMTX Provider Address";
tooltip = "MediaMTX Provider Instance Address";
typeName = "STRING";
defaultValue = "localhost";
};
class GVAR(instancePort): Edit {
property = QGVAR(instancePort);
displayname = QUOTE(MediaMTX Provider Port);
tooltip = QUOTE(MediaMTX Provider Port for handling video streams);
typeName = "STRING";
defaultValue = "8554";
};
class GVAR(instanceAuthUser): Edit {
property = QGVAR(instanceAuthUser);
displayname = QUOTE(MediaMTX Provider Username);
tooltip = QUOTE(MediaMTX Provider Instance Username);
typeName = "STRING";
defaultValue = "administrator";
};
class GVAR(instanceAuthPassword): Edit {
property = QGVAR(instanceAuthPassword);
displayname = QUOTE(MediaMTX Provider Password);
tooltip = QUOTE(MediaMTX Provider Instance Password);
typeName = "STRING";
defaultValue = "password";
};
};
*/
};
};

View File

@@ -1 +0,0 @@
PREP(videoParser);

View File

@@ -1,9 +0,0 @@
#include "script_component.hpp"
ADDON = false;
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
ADDON = true;

View File

@@ -1,3 +0,0 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@@ -1,23 +0,0 @@
#include "script_component.hpp"
class CfgPatches {
class ADDON {
name = COMPONENT_NAME;
units[] = {
QGVAR(videoModule)
};
weapons[] = {};
requiredAddons[] = {
"cba_main",
"ace_main",
"armatak_main",
"armatak_server"
};
requiredVersion = REQUIRED_VERSION;
author = PROJECT_AUTHOR;
url = "https://github.com/valmojr/armatak";
};
};
#include "CfgEventHandlers.hpp"
//#include "CfgVehicles.hpp"

View File

@@ -1,83 +0,0 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
private _instance_address = GETVAR(_logic,GVAR(instanceAddress),false);
private _instance_port = GETVAR(_logic,GVAR(instancePort),false);
private _instance_auth_user = GETVAR(_logic,GVAR(instanceAuthUser),false);
private _instance_auth_pass = GETVAR(_logic,GVAR(instanceAuthPassword),false);
SETMVAR(GVAR(instanceAddress),_instance_address);
SETMVAR(GVAR(instancePort),_instance_port);
SETMVAR(GVAR(instanceAuthUser),_instance_auth_user);
SETMVAR(GVAR(instanceAuthPassword),_instance_auth_pass);
_startAction = [
QGVAR(startStream),
"Start Video Feed",
"",
{
_uuid = (_this select 0) call armatak_fnc_extract_uuid;
_uuid_short = _uuid select [0, 8];
_role = roleDescription (_this select 0);
_name = name (_this select 0);
_role = [_role] call BIS_fnc_filterString;
_name = [_name] call BIS_fnc_filterString;
_stream_path = _name + "_" + _role + "_" + _uuid_short;
armatak_mediamtx_video_stream_instance_address = GETMVAR(instance_address,false);
armatak_mediamtx_video_stream_instance_port = missionNamespace getVariable "instance_port";
armatak_mediamtx_video_stream_instance_auth_user = missionNamespace getVariable "instance_auth_user";
armatak_mediamtx_video_stream_instance_auth_pass = missionNamespace getVariable "instance_auth_pass";
"armatak" callExtension ["video_stream:start", [armatak_mediamtx_video_stream_instance_address, armatak_mediamtx_video_stream_instance_port, _stream_path, armatak_mediamtx_video_stream_instance_auth_user, armatak_mediamtx_video_stream_instance_auth_pass]];
(_this select 0) setVariable ["armatak_video_feed_is_streaming", true];
},
{
(_this select 0) getVariable "armatak_video_feed_is_streaming" == false
}
] call ace_interact_menu_fnc_createAction;
[
"Man",
1,
["ACE_SelfActions"],
_startAction,
true
] call ace_interact_menu_fnc_addActionToClass;
_stopAction = [
"ArmatakStopStream",
"Stop Video Feed",
"",
{
"armatak" callExtension ["video_stream:stop", []];
SETVAR(_this select 0,GVAR(isStreaming),false);
},
{
GETVAR((this select 0),GVAR(isStreaming),false)
}
] call ace_interact_menu_fnc_createAction;
[
"Man",
1,
["ACE_SelfActions"],
_stopAction,
true
] call ace_interact_menu_fnc_addActionToClass;
if (isMultiplayer) then {
{
SETVAR(_x,GVAR(isStreaming),false);
} forEach playableUnits;
} else {
SETVAR(player,GVAR(isStreaming),false);
};
};
true;

View File

@@ -1,17 +0,0 @@
#define COMPONENT video
#define COMPONENT_BEAUTIFIED Video Streaming
#include "\armatak\armatak\addons\main\script_mod.hpp"
// #define DEBUG_MODE_FULL
// #define DISABLE_COMPILE_CACHE
// #define ENABLE_PERFORMANCE_COUNTERS
#ifdef DEBUG_ENABLED_MAIN
#define DEBUG_MODE_FULL
#endif
#ifdef DEBUG_SETTINGS_MAIN
#define DEBUG_SETTINGS DEBUG_SETTINGS_MAIN
#endif
#include "\z\ace\addons\main\script_macros.hpp"

BIN
media/delta_larp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
media/picture.png LFS Normal file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
use uuid::Uuid;
use chrono::{Duration, SecondsFormat, Utc}; use chrono::{Duration, SecondsFormat, Utc};
use uuid::Uuid;
pub struct CursorOverTime { pub struct CursorOverTime {
pub uuid: Option<String>, pub uuid: Option<String>,
@@ -16,9 +16,18 @@ pub struct CursorOverTime {
pub track_speed: Option<f32>, pub track_speed: Option<f32>,
pub link_uid: Option<String>, pub link_uid: Option<String>,
pub remarker: Option<String>, pub remarker: Option<String>,
pub video_url: Option<String>,
} }
impl CursorOverTime { impl CursorOverTime {
fn escape_xml_attribute(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
pub fn convert_to_xml(&self) -> String { pub fn convert_to_xml(&self) -> String {
let uuid = match &self.uuid { let uuid = match &self.uuid {
Some(uuid) => uuid, Some(uuid) => uuid,
@@ -107,6 +116,18 @@ impl CursorOverTime {
xml.push_str(format!("<remarks>ARMATAK | {}</remarks>", remark).as_str()); xml.push_str(format!("<remarks>ARMATAK | {}</remarks>", remark).as_str());
} }
if let Some(video_url) = &self.video_url {
if !video_url.trim().is_empty() {
xml.push_str(
format!(
"<__video url=\"{}\" />",
Self::escape_xml_attribute(video_url.trim())
)
.as_str(),
);
}
}
xml.push_str("</detail></event>"); xml.push_str("</detail></event>");
return xml; return xml;

View File

@@ -1,45 +1,47 @@
use arma_rs::{FromArma, FromArmaError};
use super::cot::CursorOverTime; use super::cot::CursorOverTime;
use arma_rs::{FromArma, FromArmaError};
pub struct DigitalPointerPayload { pub struct DigitalPointerPayload {
pub link_uid: String, pub link_uid: String,
pub contact_callsign: String, pub contact_callsign: String,
pub point_lat: f64, pub point_lat: f64,
pub point_lon: f64, pub point_lon: f64,
pub point_hae: f32, pub point_hae: f32,
} }
impl FromArma for DigitalPointerPayload { impl FromArma for DigitalPointerPayload {
fn from_arma(data: String) -> Result<DigitalPointerPayload, FromArmaError> { fn from_arma(data: String) -> Result<DigitalPointerPayload, FromArmaError> {
let (link_uid, contact_callsign, point_lat, point_lon, point_hae) = let (link_uid, contact_callsign, point_lat, point_lon, point_hae) =
<(String, String, f64, f64, f32)>::from_arma(data)?; <(String, String, f64, f64, f32)>::from_arma(data)?;
Ok(Self { Ok(Self {
link_uid, link_uid,
contact_callsign, contact_callsign,
point_lat, point_lat,
point_lon, point_lon,
point_hae, point_hae,
}) })
} }
} }
impl DigitalPointerPayload { impl DigitalPointerPayload {
pub fn to_cot(&self) -> CursorOverTime { pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime { CursorOverTime {
uuid: Some(format!("{}{}", self.link_uid.clone(), ".SPI1")), uuid: Some(format!("{}{}", self.link_uid.clone(), ".SPI1")),
r#type: Some("b-m-p-s-p-i".to_string()), r#type: Some("b-m-p-s-p-i".to_string()),
point_lat: self.point_lat, point_lat: self.point_lat,
point_lon: self.point_lon, point_lon: self.point_lon,
point_hae: self.point_hae, point_hae: self.point_hae,
point_ce: None, point_ce: None,
point_le: None, point_le: None,
contact_callsign: self.contact_callsign.clone(), contact_callsign: self.contact_callsign.clone(),
group_name: None, group_name: None,
group_role: None, group_role: None,
track_course: None, track_course: None,
track_speed: None, track_speed: None,
link_uid: Some(self.link_uid.clone()), link_uid: Some(self.link_uid.clone()),
remarker: None, remarker: None,
} video_url: None,
} }
}
} }

View File

@@ -1 +1 @@
pub mod circle; pub mod circle;

View File

@@ -1,62 +1,64 @@
use arma_rs::{FromArma, FromArmaError};
use super::cot::CursorOverTime; use super::cot::CursorOverTime;
use arma_rs::{FromArma, FromArmaError};
pub struct EudCoTPayload { pub struct EudCoTPayload {
pub uuid: String, pub uuid: String,
pub point_lat: f64, pub point_lat: f64,
pub point_lon: f64, pub point_lon: f64,
pub point_hae: f32, pub point_hae: f32,
pub contact_callsign: String, pub contact_callsign: String,
pub group_name: String, pub group_name: String,
pub group_role: String, pub group_role: String,
pub track_course: i32, pub track_course: i32,
pub track_speed: f32, pub track_speed: f32,
} }
impl FromArma for EudCoTPayload { impl FromArma for EudCoTPayload {
fn from_arma(data: String) -> Result<EudCoTPayload, FromArmaError> { fn from_arma(data: String) -> Result<EudCoTPayload, FromArmaError> {
let ( let (
uuid, uuid,
point_lat, point_lat,
point_lon, point_lon,
point_hae, point_hae,
contact_callsign, contact_callsign,
group_name, group_name,
group_role, group_role,
track_course, track_course,
track_speed, track_speed,
) = <(String, f64, f64, f32, String, String, String, i32, f32)>::from_arma(data)?; ) = <(String, f64, f64, f32, String, String, String, i32, f32)>::from_arma(data)?;
Ok(Self { Ok(Self {
uuid, uuid,
point_lat, point_lat,
point_lon, point_lon,
point_hae, point_hae,
contact_callsign, contact_callsign,
group_name, group_name,
group_role, group_role,
track_course, track_course,
track_speed, track_speed,
}) })
} }
} }
impl EudCoTPayload { impl EudCoTPayload {
pub fn to_cot(&self) -> CursorOverTime { pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime { CursorOverTime {
uuid: Some(self.uuid.clone()), uuid: Some(self.uuid.clone()),
r#type: None, r#type: None,
point_lat: self.point_lat, point_lat: self.point_lat,
point_lon: self.point_lon, point_lon: self.point_lon,
point_hae: self.point_hae, point_hae: self.point_hae,
point_ce: None, point_ce: None,
point_le: None, point_le: None,
contact_callsign: self.contact_callsign.clone(), contact_callsign: self.contact_callsign.clone(),
group_name: Some(self.group_name.clone()), group_name: Some(self.group_name.clone()),
group_role: Some(self.group_role.clone()), group_role: Some(self.group_role.clone()),
track_course: Some(self.track_course), track_course: Some(self.track_course),
track_speed: Some(self.track_speed), track_speed: Some(self.track_speed),
link_uid: None, link_uid: None,
remarker: None, remarker: None,
} video_url: None,
} }
}
} }

View File

@@ -1,5 +1,5 @@
use arma_rs::{FromArma, FromArmaError};
use super::cot::CursorOverTime; use super::cot::CursorOverTime;
use arma_rs::{FromArma, FromArmaError};
pub struct ExternalPositionPayload { pub struct ExternalPositionPayload {
pub uuid: String, pub uuid: String,
@@ -13,47 +13,49 @@ pub struct ExternalPositionPayload {
} }
impl FromArma for ExternalPositionPayload { impl FromArma for ExternalPositionPayload {
fn from_arma(data: String) -> Result<ExternalPositionPayload, FromArmaError> { fn from_arma(data: String) -> Result<ExternalPositionPayload, FromArmaError> {
let ( let (
uuid, uuid,
point_lat, point_lat,
point_lon, point_lon,
point_hae, point_hae,
contact_callsign, contact_callsign,
track_course, track_course,
track_speed, track_speed,
remarker, remarker,
) = <(String, f64, f64, f32, String, i32, f32, String)>::from_arma(data)?; ) = <(String, f64, f64, f32, String, i32, f32, String)>::from_arma(data)?;
Ok(Self { Ok(Self {
uuid, uuid,
point_lat, point_lat,
point_lon, point_lon,
point_hae, point_hae,
contact_callsign, contact_callsign,
track_course, track_course,
track_speed, track_speed,
remarker, remarker,
}) })
} }
} }
impl ExternalPositionPayload { impl ExternalPositionPayload {
pub fn to_cot(&self) -> CursorOverTime { pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime { CursorOverTime {
uuid: Some(self.uuid.clone()), uuid: Some(self.uuid.clone()),
r#type: None, r#type: None,
point_lat: self.point_lat, point_lat: self.point_lat,
point_lon: self.point_lon, point_lon: self.point_lon,
point_hae: self.point_hae, point_hae: self.point_hae,
point_ce: None, point_ce: None,
point_le: None, point_le: None,
contact_callsign: self.contact_callsign.clone(), contact_callsign: self.contact_callsign.clone(),
group_name: None, group_name: None,
group_role: None, group_role: None,
track_course: Some(self.track_course), track_course: Some(self.track_course),
track_speed: Some(self.track_speed), track_speed: Some(self.track_speed),
link_uid: None, link_uid: None,
remarker: Some(self.remarker.clone()), remarker: Some(self.remarker.clone()),
} video_url: None,
} }
}
} }

View File

@@ -1,5 +1,5 @@
use arma_rs::{FromArma, FromArmaError}; use arma_rs::{FromArma, FromArmaError};
use chrono::{Utc, Duration, SecondsFormat}; use chrono::{Duration, SecondsFormat, Utc};
use uuid::Uuid; use uuid::Uuid;
pub struct MessagePayload { pub struct MessagePayload {
@@ -14,8 +14,7 @@ pub struct MessagePayload {
impl FromArma for MessagePayload { impl FromArma for MessagePayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> { fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (sender_callsign, chatroom, message_text, let (sender_callsign, chatroom, message_text, point_lat, point_lon, point_hae, sender_uid) =
point_lat, point_lon, point_hae, sender_uid) =
<(String, String, String, f64, f64, f32, String)>::from_arma(data)?; <(String, String, String, f64, f64, f32, String)>::from_arma(data)?;
Ok(Self { Ok(Self {
@@ -55,8 +54,8 @@ impl MessageCot {
pub fn to_xml(&self) -> String { pub fn to_xml(&self) -> String {
let created_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); let created_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let stale_time = (Utc::now() + Duration::days(1)) let stale_time =
.to_rfc3339_opts(SecondsFormat::Millis, true); (Utc::now() + Duration::days(1)).to_rfc3339_opts(SecondsFormat::Millis, true);
// MESSAGE ID (random UUID) // MESSAGE ID (random UUID)
let message_uuid = Uuid::new_v4().to_string(); let message_uuid = Uuid::new_v4().to_string();
@@ -98,10 +97,7 @@ impl MessageCot {
format!( format!(
"<__chat parent=\"RootContactGroup\" groupOwner=\"false\" \ "<__chat parent=\"RootContactGroup\" groupOwner=\"false\" \
messageId=\"{}\" chatroom=\"{}\" id=\"{}\" senderCallsign=\"{}\">", messageId=\"{}\" chatroom=\"{}\" id=\"{}\" senderCallsign=\"{}\">",
message_uuid, message_uuid, self.chatroom, self.chatroom, self.sender_callsign,
self.chatroom,
self.chatroom,
self.sender_callsign,
) )
.as_str(), .as_str(),
); );
@@ -109,9 +105,7 @@ impl MessageCot {
xml.push_str( xml.push_str(
format!( format!(
"<chatgrp uid0=\"{}\" uid1=\"{}\" id=\"{}\" />", "<chatgrp uid0=\"{}\" uid1=\"{}\" id=\"{}\" />",
self.sender_uid, self.sender_uid, self.chatroom, self.chatroom
self.chatroom,
self.chatroom
) )
.as_str(), .as_str(),
); );

View File

@@ -1,7 +1,8 @@
pub mod draws;
pub mod cot; pub mod cot;
pub mod digital_pointer; pub mod digital_pointer;
pub mod draws;
pub mod eud; pub mod eud;
pub mod gps; pub mod gps;
pub mod message; pub mod message;
pub mod nato; pub mod nato;
pub mod uas;

View File

@@ -11,10 +11,40 @@ pub struct MarkerCoTPayload {
pub contact_callsign: String, pub contact_callsign: String,
pub track_course: i32, pub track_course: i32,
pub track_speed: f32, pub track_speed: f32,
pub video_url: Option<String>,
} }
impl FromArma for MarkerCoTPayload { impl FromArma for MarkerCoTPayload {
fn from_arma(data: String) -> Result<MarkerCoTPayload, FromArmaError> { fn from_arma(data: String) -> Result<MarkerCoTPayload, FromArmaError> {
if let Ok((
uuid,
r#type,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
video_url,
)) = <(String, String, f64, f64, f32, String, i32, f32, String)>::from_arma(data.clone())
{
return Ok(Self {
uuid,
r#type,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
video_url: if video_url.trim().is_empty() {
None
} else {
Some(video_url)
},
});
}
let ( let (
uuid, uuid,
r#type, r#type,
@@ -34,6 +64,7 @@ impl FromArma for MarkerCoTPayload {
contact_callsign, contact_callsign,
track_course, track_course,
track_speed, track_speed,
video_url: None,
}) })
} }
} }
@@ -55,6 +86,7 @@ impl MarkerCoTPayload {
track_speed: Some(self.track_speed), track_speed: Some(self.track_speed),
link_uid: None, link_uid: None,
remarker: None, remarker: None,
video_url: self.video_url.clone(),
} }
} }
} }

243
src/cot/uas.rs Normal file
View File

@@ -0,0 +1,243 @@
// src/cot/uas.rs
//
// CoT types required for ATAK UAS Tool integration.
//
// Two event types are needed so that the UAS Tool plugin recognises a drone:
//
// b-i-v — Video endpoint declaration. Tells the UAS Tool where
// to pull the RTSP stream for this drone.
//
// b-m-p-s-p-loc — Sensor position event. Carries the camera azimuth,
// field-of-view, and slant-range that the UAS Tool uses
// to draw the FOV cone on the map and to project AR
// markers onto the video feed.
//
// The two events are linked: the b-m-p-s-p-loc detail contains
// <__video uid="<drone-uuid>"/>
// which references the uid of the b-i-v event, so the UAS Tool knows which
// video stream belongs to this sensor.
use arma_rs::{FromArma, FromArmaError};
use chrono::{Duration, SecondsFormat, Utc};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Parse an RTSP URL of the form rtsp://address:port/path
/// into its three components.
fn parse_rtsp_url(url: &str) -> Option<(String, String, String)> {
let without_proto = url.strip_prefix("rtsp://")?;
let slash_pos = without_proto.find('/')?;
let host_port = &without_proto[..slash_pos];
let path = &without_proto[slash_pos..]; // includes the leading '/'
let colon_pos = host_port.rfind(':')?;
let address = host_port[..colon_pos].to_string();
let port = host_port[colon_pos + 1..].to_string();
Some((address, port, path.to_string()))
}
// ---------------------------------------------------------------------------
// b-i-v Video endpoint declaration
// ---------------------------------------------------------------------------
pub struct UasVideoCoTPayload {
/// The drone's persistent ATAK UUID (same uid used for PPLI / marker CoT).
pub uid: String,
/// Human-readable label shown in the UAS Tool video list.
pub callsign: String,
/// Full RTSP URL, e.g. "rtsp://192.168.1.10:8554/live/drone1".
pub video_url: String,
}
impl FromArma for UasVideoCoTPayload {
fn from_arma(data: String) -> Result<UasVideoCoTPayload, FromArmaError> {
let (uid, callsign, video_url) =
<(String, String, String)>::from_arma(data)?;
Ok(Self {
uid,
callsign,
video_url,
})
}
}
impl UasVideoCoTPayload {
/// Build the complete XML string for the b-i-v CoT event.
/// Returns an empty string if the RTSP URL cannot be parsed.
pub fn to_xml(&self) -> String {
let (address, port, path) = match parse_rtsp_url(&self.video_url) {
Some(parts) => parts,
None => {
log::warn!(
"UasVideoCoTPayload: could not parse RTSP URL: {}",
self.video_url
);
return String::new();
}
};
let now =
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
// Long stale time: the video endpoint is considered valid for 1 hour.
// The CoT is re-sent every router tick so it stays fresh even if the
// TAK server restarts.
let stale = (Utc::now() + Duration::seconds(3600))
.to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!(
"<event type=\"b-i-v\" version=\"2.0\" how=\"m-g\" \
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid = self.uid,
now = now,
stale = stale
));
// b-i-v events carry no real geographic position.
xml.push_str(
"<point lat=\"0\" lon=\"0\" hae=\"9999999.0\" \
ce=\"9999999.0\" le=\"9999999.0\"/>",
);
xml.push_str("<detail>");
xml.push_str("<__video>");
xml.push_str(&format!(
"<ConnectionEntry \
protocol=\"rtsp\" \
path=\"{path}\" \
address=\"{address}\" \
port=\"{port}\" \
uid=\"{uid}\" \
alias=\"{callsign}\" \
roverPort=\"-1\" \
rtspReliable=\"0\" \
ignoreEmbeddedKLV=\"False\" \
networkTimeout=\"0\" \
bufferTime=\"-1\"/>",
path = path,
address = address,
port = port,
uid = self.uid,
callsign = self.callsign,
));
xml.push_str("</__video>");
xml.push_str(&format!(
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>");
xml.push_str("</event>");
xml
}
}
// ---------------------------------------------------------------------------
// b-m-p-s-p-loc Sensor position (FOV cone + video link)
// ---------------------------------------------------------------------------
pub struct UasSensorCoTPayload {
/// UID for this sensor event — conventionally "<drone-uuid>-sensor".
pub uid: String,
/// The drone's ATAK UUID; must match the uid used in the b-i-v event so
/// the UAS Tool can link sensor data to the correct video stream.
pub video_uid: String,
/// Callsign shown in the UAS Tool sensor list.
pub callsign: String,
/// Drone latitude in decimal degrees (WGS-84).
pub point_lat: f64,
/// Drone longitude in decimal degrees (WGS-84).
pub point_lon: f64,
/// Drone height above ellipsoid in metres (WGS-84).
pub point_hae: f32,
/// Camera azimuth in degrees, clockwise from true North (0359).
pub azimuth: i32,
/// Camera horizontal field of view in degrees.
pub fov: i32,
/// Estimated slant range from drone to ground point in metres.
/// A good approximation is the drone's AGL altitude.
pub range: i32,
}
impl FromArma for UasSensorCoTPayload {
fn from_arma(data: String) -> Result<UasSensorCoTPayload, FromArmaError> {
let (
uid,
video_uid,
callsign,
point_lat,
point_lon,
point_hae,
azimuth,
fov,
range,
) = <(String, String, String, f64, f64, f32, i32, i32, i32)>::from_arma(
data,
)?;
Ok(Self {
uid,
video_uid,
callsign,
point_lat,
point_lon,
point_hae,
azimuth,
fov,
range,
})
}
}
impl UasSensorCoTPayload {
/// Build the complete XML string for the b-m-p-s-p-loc CoT event.
pub fn to_xml(&self) -> String {
let now =
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
// 60-second stale: must be refreshed every router tick (1 s) to keep
// the FOV cone visible on the map.
let stale = (Utc::now() + Duration::seconds(60))
.to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!(
"<event type=\"b-m-p-s-p-loc\" version=\"2.0\" how=\"h-g-i-g-o\" \
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid = self.uid,
now = now,
stale = stale,
));
xml.push_str(&format!(
"<point lat=\"{lat}\" lon=\"{lon}\" hae=\"{hae}\" \
ce=\"9999999.0\" le=\"9999999.0\"/>",
lat = self.point_lat,
lon = self.point_lon,
hae = self.point_hae,
));
xml.push_str("<detail>");
// fovAlpha controls the transparency of the FOV cone fill (01).
// 0.537 ≈ 137/255, the value used by the real UAS Tool.
xml.push_str(&format!(
"<sensor \
fov=\"{fov}\" \
fovRed=\"1\" \
fovGreen=\"1\" \
fovBlue=\"1\" \
fovAlpha=\"0.5372549\" \
displayMagneticReference=\"0\" \
range=\"{range}\" \
azimuth=\"{az}\"/>",
fov = self.fov,
range = self.range,
az = self.azimuth,
));
// Link this sensor event to the b-i-v video endpoint.
xml.push_str(&format!("<__video uid=\"{}\"/>", self.video_uid));
xml.push_str(&format!(
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>");
xml.push_str("</event>");
xml
}
}

View File

@@ -1,4 +1,5 @@
use arma_rs::{arma, Extension, Group}; use arma_rs::{arma, Extension, Group};
use rustls::crypto::aws_lc_rs;
mod structs; mod structs;
mod tcp; mod tcp;
mod tests; mod tests;
@@ -31,6 +32,9 @@ pub fn init() -> Extension {
log4rs::init_config(config).unwrap(); log4rs::init_config(config).unwrap();
let _ = aws_lc_rs::default_provider().install_default();
log::info!("Initialized rustls aws-lc crypto provider.");
Extension::build() Extension::build()
.command("local_ip", utils::address::get_local_address) .command("local_ip", utils::address::get_local_address)
.command("uuid", utils::uuid::get_uuid) .command("uuid", utils::uuid::get_uuid)
@@ -47,6 +51,8 @@ pub fn init() -> Extension {
"tcp_socket", "tcp_socket",
Group::new() Group::new()
.command("start", tcp::start) .command("start", tcp::start)
.command("start_mtls", tcp::start_mtls)
.command("start_enroll_mtls", tcp::start_enroll_mtls)
.command("stop", tcp::stop) .command("stop", tcp::stop)
.command("send_payload", tcp::send_payload) .command("send_payload", tcp::send_payload)
.group( .group(
@@ -55,7 +61,10 @@ pub fn init() -> Extension {
.command("eud", tcp::cot::send_eud_cot) .command("eud", tcp::cot::send_eud_cot)
.command("marker", tcp::cot::send_marker_cot) .command("marker", tcp::cot::send_marker_cot)
.command("digital_pointer", tcp::cot::send_digital_pointer_cot) .command("digital_pointer", tcp::cot::send_digital_pointer_cot)
.command("chat", tcp::cot::send_message_cot), .command("chat", tcp::cot::send_message_cot)
// UAS Tool integration
.command("uas_video", tcp::cot::send_uas_video_cot)
.command("uas_sensor", tcp::cot::send_uas_sensor_cot),
) )
.group( .group(
"draw", "draw",

View File

@@ -10,9 +10,6 @@ pub struct LogPayload {
impl FromArma for LogPayload { impl FromArma for LogPayload {
fn from_arma(data: String) -> Result<LogPayload, FromArmaError> { fn from_arma(data: String) -> Result<LogPayload, FromArmaError> {
let (status, message) = <(String, String)>::from_arma(data)?; let (status, message) = <(String, String)>::from_arma(data)?;
Ok(Self { Ok(Self { status, message })
status,
message
})
} }
} }

280
src/tcp/client.rs Normal file
View File

@@ -0,0 +1,280 @@
use arma_rs::Context;
use log::{info, warn};
use std::collections::VecDeque;
use std::panic::{self, AssertUnwindSafe};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, TryRecvError};
use std::thread;
use std::time::Duration;
use super::config::ConnectionConfig;
use super::transport::{connect_stream, TransportStream};
use super::TCP_CLIENT;
const CONNECT_POLL_INTERVAL: Duration = Duration::from_millis(200);
const MAX_PENDING_MESSAGES: usize = 128;
pub enum TcpCommand {
SendMessage(String, Context),
Stop,
}
pub struct TcpClient {
pub(crate) tx: Sender<TcpCommand>,
}
enum ConnectionState {
Connecting,
Connected,
Failed(String),
}
enum ConnectEvent {
Connected(TransportStream),
Failed(String),
}
fn describe_panic_payload(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"unknown panic payload".to_string()
}
}
fn log_message_preview(message: &str) -> String {
message.chars().take(96).collect::<String>()
}
fn send_over_stream(
stream: &mut TransportStream,
context: &Context,
message: String,
) -> Result<(), String> {
let message_len = message.len();
info!("Sending TCP payload ({} bytes)", message_len);
stream
.write_message(message.as_bytes())
.map_err(|e| {
let message = e.to_string();
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket disconnected",
message.clone(),
);
message
})
}
fn flush_pending_messages(
connection: &mut Option<TransportStream>,
pending_messages: &mut VecDeque<(String, Context)>,
state: &mut ConnectionState,
) {
if pending_messages.is_empty() {
return;
}
let Some(stream) = connection.as_mut() else {
return;
};
info!(
"Flushing {} queued TCP payload(s) after connection became active",
pending_messages.len()
);
while let Some((message, context)) = pending_messages.pop_front() {
if let Err(error) = send_over_stream(stream, &context, message) {
info!("Failed to send queued message: {}", error);
*state = ConnectionState::Failed(error);
*connection = None;
return;
}
}
}
fn poll_connect_event(
connect_rx: &Receiver<ConnectEvent>,
connection: &mut Option<TransportStream>,
state: &mut ConnectionState,
pending_messages: &mut VecDeque<(String, Context)>,
ctx: &Context,
connection_message: &str,
target: &str,
) {
loop {
match connect_rx.try_recv() {
Ok(ConnectEvent::Connected(stream)) => {
info!("TCP connection established successfully: {}", target);
let _ = ctx.callback_data("TCP SOCKET", connection_message, target.to_string());
*connection = Some(stream);
*state = ConnectionState::Connected;
flush_pending_messages(connection, pending_messages, state);
}
Ok(ConnectEvent::Failed(error)) => {
info!("Failed to connect to TCP server: {}", error);
let _ = ctx.callback_data(
"TCP SOCKET ERROR",
"TAK Socket connection failed",
error.clone(),
);
*state = ConnectionState::Failed(error);
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => break,
}
}
}
impl TcpClient {
pub fn start(&self, config: ConnectionConfig, rx: Receiver<TcpCommand>, ctx: Context) {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
info!("Existing TCP client detected; stopping previous instance before restart.");
client.stop();
}
thread::spawn(move || {
let mut running = true;
let connection_message = config.connected_message();
let config_description = config.describe();
let target = config.target();
let mut state = ConnectionState::Connecting;
let mut connection: Option<TransportStream> = None;
let mut pending_messages: VecDeque<(String, Context)> = VecDeque::new();
let (connect_tx, connect_rx) = mpsc::channel();
info!("TCP worker thread started with config: {}", config_description);
let tcp_thread = thread::spawn(move || {
let connect_result = panic::catch_unwind(AssertUnwindSafe(|| connect_stream(&config)));
match connect_result {
Ok(Ok(stream)) => {
let _ = connect_tx.send(ConnectEvent::Connected(stream));
}
Ok(Err(error)) => {
let _ = connect_tx.send(ConnectEvent::Failed(error));
}
Err(payload) => {
let message = format!(
"TCP connection worker panicked: {}",
describe_panic_payload(payload)
);
let _ = connect_tx.send(ConnectEvent::Failed(message));
}
}
});
while running {
poll_connect_event(
&connect_rx,
&mut connection,
&mut state,
&mut pending_messages,
&ctx,
connection_message,
&target,
);
match rx.recv_timeout(CONNECT_POLL_INTERVAL) {
Ok(TcpCommand::SendMessage(message, context)) => {
let message_len = message.len();
match &mut state {
ConnectionState::Connected => {
if let Some(stream) = connection.as_mut() {
if let Err(error) = send_over_stream(stream, &context, message) {
info!("Failed to send message: {}", error);
state = ConnectionState::Failed(error);
connection = None;
}
} else {
warn!(
"Connection state said connected, but no socket was present; queuing payload."
);
pending_messages.push_back((message, context));
}
}
ConnectionState::Connecting => {
if pending_messages.len() >= MAX_PENDING_MESSAGES {
let preview = log_message_preview(&message);
warn!(
"Dropping TCP payload because connection is still pending and queue is full ({} bytes, preview={:?})",
message_len, preview
);
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket is still connecting",
format!(
"queue full while connecting; dropped payload ({} bytes, preview={:?})",
message_len, preview
),
);
} else {
info!(
"Queueing TCP payload while connection is pending ({} bytes, queued={})",
message_len,
pending_messages.len() + 1
);
pending_messages.push_back((message, context));
}
}
ConnectionState::Failed(error) => {
let preview = log_message_preview(&message);
warn!(
"Dropping TCP payload because connection is in failed state ({} bytes, preview={:?}, error={})",
message_len, preview, error
);
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket is not connected",
error.clone(),
);
}
}
}
Ok(TcpCommand::Stop) => {
running = false;
info!("Stopping TCP client.");
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => {
warn!("TCP command channel disconnected.");
running = false;
}
}
}
info!("Waiting for TCP connection thread to finish.");
match tcp_thread.join() {
Ok(()) => info!("TCP connection thread joined successfully."),
Err(payload) => warn!(
"TCP connection thread join reported a panic: {}",
describe_panic_payload(payload)
),
}
info!("TCP worker thread finished.");
});
}
pub fn send_payload(&self, context: Context, payload: String) {
let tx = self.tx.clone();
thread::spawn(move || {
info!("Dispatching queued TCP payload command.");
if let Err(error) = tx.send(TcpCommand::SendMessage(payload, context)) {
warn!("Failed to dispatch TCP payload command: {}", error);
}
});
}
pub fn stop(&self) {
let tx = self.tx.clone();
thread::spawn(move || {
info!("Dispatching TCP stop command.");
if let Err(error) = tx.send(TcpCommand::Stop) {
warn!("Failed to dispatch TCP stop command: {}", error);
}
});
}
}

64
src/tcp/config.rs Normal file
View File

@@ -0,0 +1,64 @@
pub enum ConnectionConfig {
Plain {
address: String,
},
Mtls {
address: String,
server_name: String,
ca_cert_path: String,
client_cert_path: String,
client_key_path: String,
},
EnrollMtls {
host: String,
server_name: String,
enroll_port: String,
username: String,
password: String,
client_uid: String,
},
}
impl ConnectionConfig {
pub fn connected_message(&self) -> &'static str {
match self {
Self::Plain { .. } => "Connected to TCP Server",
Self::Mtls { .. } => "Connected to TAK Server via mTLS",
Self::EnrollMtls { .. } => "Connected to TAK Server via enrolled mTLS certificate",
}
}
pub fn target(&self) -> String {
match self {
Self::Plain { address } | Self::Mtls { address, .. } => address.clone(),
Self::EnrollMtls { host, .. } => host.clone(),
}
}
pub fn describe(&self) -> String {
match self {
Self::Plain { address } => format!("plain tcp -> {}", address),
Self::Mtls {
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
} => format!(
"manual mtls -> {} (server_name={}, ca={}, cert={}, key={})",
address, server_name, ca_cert_path, client_cert_path, client_key_path
),
Self::EnrollMtls {
host,
server_name,
enroll_port,
username,
client_uid,
..
} => format!(
"enroll mtls -> host={} enroll_port={} server_name={} username={} client_uid={}",
host, enroll_port, server_name, username, client_uid
),
}
}
}

View File

@@ -9,24 +9,62 @@ pub fn send_eud_cot(ctx: Context, cursor_over_time: cot::eud::EudCoTPayload) ->
"Sending End User Device Cursor Over Time to TCP server" "Sending End User Device Cursor Over Time to TCP server"
} }
pub fn send_marker_cot(ctx: Context, cursor_over_time: cot::nato::MarkerCoTPayload) -> &'static str { pub fn send_marker_cot(
ctx: Context,
cursor_over_time: cot::nato::MarkerCoTPayload,
) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml(); let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload); send_payload(ctx, payload);
"Sending Marker Cursor Over Time to TCP server" "Sending Marker Cursor Over Time to TCP server"
} }
pub fn send_digital_pointer_cot(ctx: Context, cursor_over_time: cot::digital_pointer::DigitalPointerPayload) -> &'static str { pub fn send_digital_pointer_cot(
ctx: Context,
cursor_over_time: cot::digital_pointer::DigitalPointerPayload,
) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml(); let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload); send_payload(ctx, payload);
"Sending Digital Pointer Cursor Over Time to TCP server" "Sending Digital Pointer Cursor Over Time to TCP server"
} }
pub fn send_message_cot(ctx: Context, message_payload: cot::message::MessagePayload) -> &'static str { pub fn send_message_cot(
ctx: Context,
message_payload: cot::message::MessagePayload,
) -> &'static str {
let message_cot = cot::message::MessageCot::from_payload(message_payload); let message_cot = cot::message::MessageCot::from_payload(message_payload);
let payload = message_cot.to_xml(); let payload = message_cot.to_xml();
send_payload(ctx, payload); send_payload(ctx, payload);
"Sending Message CoT to TCP server" "Sending Message CoT to TCP server"
} }
/// Send a b-i-v CoT that declares the RTSP video endpoint for a drone.
/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_video", [payload]]
///
/// Returns early without sending if the RTSP URL in the payload cannot be parsed.
pub fn send_uas_video_cot(
ctx: Context,
payload: cot::uas::UasVideoCoTPayload,
) -> &'static str {
let xml = payload.to_xml();
if !xml.is_empty() {
send_payload(ctx, xml);
}
"Sending UAS Video (b-i-v) CoT to TCP server"
}
/// Send a b-m-p-s-p-loc CoT carrying the drone camera's azimuth, FOV, and
/// slant-range so the UAS Tool can draw the FOV cone on the map.
/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_sensor", [payload]]
pub fn send_uas_sensor_cot(
ctx: Context,
payload: cot::uas::UasSensorCoTPayload,
) -> &'static str {
let xml = payload.to_xml();
send_payload(ctx, xml);
"Sending UAS Sensor (b-m-p-s-p-loc) CoT to TCP server"
}

View File

@@ -2,7 +2,10 @@ use arma_rs::Context;
use crate::{cot, tcp::send_payload}; use crate::{cot, tcp::send_payload};
pub fn send_circle_cot(ctx: Context, circle_payload: cot::draws::circle::CircleCoTPayload) -> &'static str { pub fn send_circle_cot(
ctx: Context,
circle_payload: cot::draws::circle::CircleCoTPayload,
) -> &'static str {
let shape_circle_cot = circle_payload.to_cot(); let shape_circle_cot = circle_payload.to_cot();
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let stale = (chrono::Utc::now() + chrono::Duration::days(1)) let stale = (chrono::Utc::now() + chrono::Duration::days(1))
@@ -14,21 +17,21 @@ pub fn send_circle_cot(ctx: Context, circle_payload: cot::draws::circle::CircleC
} }
pub fn send_ellipse_cot(ctx: Context) -> &'static str { pub fn send_ellipse_cot(ctx: Context) -> &'static str {
let _ = ctx; let _ = ctx;
"Not implemented: send_ellipse_cot" "Not implemented: send_ellipse_cot"
} }
pub fn send_rectangle_cot(ctx: Context) -> &'static str { pub fn send_rectangle_cot(ctx: Context) -> &'static str {
let _ = ctx; let _ = ctx;
"Not implemented: send_ellipse_cot" "Not implemented: send_ellipse_cot"
} }
pub fn send_freedraw_cot(ctx: Context) -> &'static str { pub fn send_freedraw_cot(ctx: Context) -> &'static str {
let _ = ctx; let _ = ctx;
"Not implemented: send_ellipse_cot" "Not implemented: send_ellipse_cot"
} }
pub fn send_vectordraw_cot(ctx: Context) -> &'static str { pub fn send_vectordraw_cot(ctx: Context) -> &'static str {
let _ = ctx; let _ = ctx;
"Not implemented: send_ellipse_cot" "Not implemented: send_ellipse_cot"
} }

View File

@@ -1,120 +1,90 @@
use arma_rs::Context; use arma_rs::Context;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::info; use log::info;
use std::io::Write;
use std::net::TcpStream;
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread;
mod client;
mod config;
mod tls;
mod transport;
pub mod cot; pub mod cot;
pub mod draw; pub mod draw;
pub enum TcpCommand { use client::{TcpClient, TcpCommand};
SendMessage(String, Context), use config::ConnectionConfig;
Stop,
}
pub struct TcpClient {
pub(crate) tx: Sender<TcpCommand>,
}
impl TcpClient {
pub fn start(&self, address: String, rx: Receiver<TcpCommand>, ctx: Context) {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
client.stop();
}
let connection = Arc::new(Mutex::new(None));
let connection_clone = Arc::clone(&connection);
thread::spawn(move || {
let mut running = true;
let tcp_thread = thread::spawn(move || match TcpStream::connect(&address) {
Ok(stream) => {
let _ = ctx.callback_data("TCP SOCKET", "Connected to TCP Server", address);
*connection_clone.lock().unwrap() = Some(stream);
}
Err(e) => {
let _ = ctx.callback_data(
"TCP SOCKET ERROR",
"TAK Socket connection failed",
e.to_string(),
);
info!("Failed to connect to TCP server: {}", e);
}
});
while running {
match rx.recv() {
Ok(TcpCommand::SendMessage(message, context)) => {
if let Some(mut stream) = connection.lock().unwrap().as_ref() {
if let Err(e) = stream.write_all(message.as_bytes()) {
info!("Failed to send message: {}", e);
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket disconnected",
e.to_string(),
);
running = false;
}
} else {
let _ = context.callback_null(
"TCP SOCKET ERROR",
"TAK Socket is not active",
);
}
}
Ok(TcpCommand::Stop) => {
running = false;
info!("Stopping TCP client.");
}
Err(error) => {
info!("Error receiving command: {}", error.to_string());
}
}
}
tcp_thread.join().unwrap();
});
}
pub fn send_payload(&self, context: Context, payload: String) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(TcpCommand::SendMessage(payload, context)).unwrap();
});
}
pub fn stop(&self) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(TcpCommand::Stop).unwrap();
});
}
}
lazy_static! { lazy_static! {
static ref TCP_CLIENT: Arc<Mutex<Option<TcpClient>>> = Arc::new(Mutex::new(None)); static ref TCP_CLIENT: Arc<Mutex<Option<TcpClient>>> = Arc::new(Mutex::new(None));
} }
pub fn start(ctx: Context, address: String) -> &'static str { fn start_with_config(ctx: Context, config: ConnectionConfig) {
info!("Starting TCP client with config: {}", config.describe());
let (tx, rx): (Sender<TcpCommand>, Receiver<TcpCommand>) = mpsc::channel(); let (tx, rx): (Sender<TcpCommand>, Receiver<TcpCommand>) = mpsc::channel();
let client = TcpClient { tx }; let client = TcpClient { tx };
client.start(address, rx, ctx); client.start(config, rx, ctx);
let mut client_guard = TCP_CLIENT.lock().unwrap(); let mut client_guard = TCP_CLIENT.lock().unwrap();
*client_guard = Some(client); *client_guard = Some(client);
}
pub fn start(ctx: Context, address: String) -> &'static str {
start_with_config(ctx, ConnectionConfig::Plain { address });
"Starting TCP Client" "Starting TCP Client"
} }
pub fn start_mtls(
ctx: Context,
address: String,
server_name: String,
ca_cert_path: String,
client_cert_path: String,
client_key_path: String,
) -> &'static str {
start_with_config(
ctx,
ConnectionConfig::Mtls {
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
},
);
"Starting mTLS TCP Client"
}
pub fn start_enroll_mtls(
ctx: Context,
host: String,
server_name: String,
enroll_port: String,
username: String,
password: String,
client_uid: String,
) -> &'static str {
start_with_config(
ctx,
ConnectionConfig::EnrollMtls {
host,
server_name,
enroll_port,
username,
password,
client_uid,
},
);
"Starting enrolled mTLS TCP Client"
}
pub fn send_payload(ctx: Context, payload: String) -> &'static str { pub fn send_payload(ctx: Context, payload: String) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() { if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
info!("Queueing TCP payload ({} bytes)", payload.len());
client.send_payload(ctx, payload); client.send_payload(ctx, payload);
} else { } else {
let _ = ctx.callback_null("TCP SOCKET ERROR", "TCP Client is not running"); let _ = ctx.callback_null("TCP SOCKET ERROR", "TCP Client is not running");
@@ -126,6 +96,7 @@ pub fn send_payload(ctx: Context, payload: String) -> &'static str {
pub fn stop(ctx: Context) -> &'static str { pub fn stop(ctx: Context) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() { if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
info!("Stopping TCP client via extension command.");
client.stop(); client.stop();
let _ = ctx.callback_null("TCP SOCKET", "TCP client stopped"); let _ = ctx.callback_null("TCP SOCKET", "TCP client stopped");
} else { } else {

221
src/tcp/tls/connector.rs Normal file
View File

@@ -0,0 +1,221 @@
use log::info;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
use rustls_pemfile::{certs, private_key};
use std::fs::File;
use std::io::BufReader;
use std::io::Cursor;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::time::Duration;
use std::sync::Arc;
use crate::tcp::transport::TransportStream;
const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const SOCKET_IO_TIMEOUT: Duration = Duration::from_secs(10);
fn load_certificates(path: &str) -> Result<Vec<CertificateDer<'static>>, String> {
let file = File::open(path).map_err(|e| format!("failed to open cert file {}: {}", path, e))?;
let mut reader = BufReader::new(file);
certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("failed to read certs from {}: {}", path, e))
}
fn load_private_key(path: &str) -> Result<PrivateKeyDer<'static>, String> {
let file = File::open(path).map_err(|e| format!("failed to open key file {}: {}", path, e))?;
let mut reader = BufReader::new(file);
private_key(&mut reader)
.map_err(|e| format!("failed to read private key from {}: {}", path, e))?
.ok_or_else(|| format!("no supported private key found in {}", path))
}
fn load_certificates_from_pem(pem: &str) -> Result<Vec<CertificateDer<'static>>, String> {
let mut reader = Cursor::new(pem.as_bytes());
certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("failed to read certs from PEM payload: {}", e))
}
fn load_private_key_from_pem(pem: &str) -> Result<PrivateKeyDer<'static>, String> {
let mut reader = Cursor::new(pem.as_bytes());
private_key(&mut reader)
.map_err(|e| format!("failed to read private key from PEM payload: {}", e))?
.ok_or_else(|| "no supported private key found in PEM payload".to_string())
}
fn infer_server_name(address: &str) -> &str {
address
.trim()
.trim_start_matches('[')
.split(']')
.next()
.unwrap_or(address)
.split(':')
.next()
.unwrap_or(address)
}
fn resolve_address(address: &str) -> Result<SocketAddr, String> {
address
.to_socket_addrs()
.map_err(|e| format!("failed to resolve {}: {}", address, e))?
.next()
.ok_or_else(|| format!("failed to resolve {}: no socket addresses returned", address))
}
fn connect_tcp(address: &str) -> Result<TcpStream, String> {
let socket_addr = resolve_address(address)?;
info!(
"Opening TCP connection to {} (resolved={}) with timeout {:?}",
address, socket_addr, TCP_CONNECT_TIMEOUT
);
let tcp_stream = TcpStream::connect_timeout(&socket_addr, TCP_CONNECT_TIMEOUT)
.map_err(|e| format!("failed to connect to {}: {}", address, e))?;
tcp_stream
.set_read_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set read timeout on {}: {}", address, e))?;
tcp_stream
.set_write_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set write timeout on {}: {}", address, e))?;
Ok(tcp_stream)
}
pub fn connect_mtls(
address: &str,
server_name: &str,
ca_cert_path: &str,
client_cert_path: &str,
client_key_path: &str,
) -> Result<TransportStream, String> {
info!(
"Connecting mTLS from file paths to {} using server_name={}",
address, server_name
);
let mut root_store = RootCertStore::empty();
let ca_certificates = load_certificates(ca_cert_path)?;
info!(
"Loaded {} CA certificate(s) from {}",
ca_certificates.len(),
ca_cert_path
);
for certificate in ca_certificates {
root_store
.add(certificate)
.map_err(|e| format!("failed to add CA certificate from {}: {}", ca_cert_path, e))?;
}
let client_certificates = load_certificates(client_cert_path)?;
info!(
"Loaded {} client certificate(s) from {}",
client_certificates.len(),
client_cert_path
);
let client_key = load_private_key(client_key_path)?;
info!("Loaded client private key from {}", client_key_path);
let tls_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certificates, client_key)
.map_err(|e| format!("failed to configure mTLS client: {}", e))?;
info!("Constructed rustls client config for {}", address);
let tcp_stream = connect_tcp(address)?;
let resolved_server_name = if server_name.trim().is_empty() {
infer_server_name(address).to_string()
} else {
server_name.trim().to_string()
};
let server_name = ServerName::try_from(resolved_server_name.clone())
.map_err(|_| format!("invalid TLS server name: {}", resolved_server_name))?;
let mut tls_stream = StreamOwned::new(
ClientConnection::new(Arc::new(tls_config), server_name)
.map_err(|e| format!("failed to create TLS client: {}", e))?,
tcp_stream,
);
info!("Starting mTLS handshake for {}", address);
while tls_stream.conn.is_handshaking() {
tls_stream
.conn
.complete_io(&mut tls_stream.sock)
.map_err(|e| format!("TLS handshake failed: {}", e))?;
}
info!("mTLS handshake completed successfully for {}", address);
Ok(TransportStream::Mtls(tls_stream))
}
pub fn connect_mtls_from_pem(
address: &str,
server_name: &str,
ca_cert_pem: &str,
client_cert_pem: &str,
client_key_pem: &str,
) -> Result<TransportStream, String> {
info!(
"Connecting mTLS from in-memory PEM payloads to {} using server_name={}",
address, server_name
);
let mut root_store = RootCertStore::empty();
let ca_certificates = load_certificates_from_pem(ca_cert_pem)?;
info!(
"Loaded {} CA certificate(s) from enrollment payload",
ca_certificates.len()
);
for certificate in ca_certificates {
root_store
.add(certificate)
.map_err(|e| format!("failed to add CA certificate from PEM payload: {}", e))?;
}
let client_certificates = load_certificates_from_pem(client_cert_pem)?;
info!(
"Loaded {} client certificate(s) from enrollment payload",
client_certificates.len()
);
let client_key = load_private_key_from_pem(client_key_pem)?;
info!("Loaded client private key from enrollment payload");
let tls_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certificates, client_key)
.map_err(|e| format!("failed to configure mTLS client: {}", e))?;
info!("Constructed rustls client config for {}", address);
let tcp_stream = connect_tcp(address)?;
let resolved_server_name = if server_name.trim().is_empty() {
infer_server_name(address).to_string()
} else {
server_name.trim().to_string()
};
let server_name = ServerName::try_from(resolved_server_name.clone())
.map_err(|_| format!("invalid TLS server name: {}", resolved_server_name))?;
let mut tls_stream = StreamOwned::new(
ClientConnection::new(Arc::new(tls_config), server_name)
.map_err(|e| format!("failed to create TLS client: {}", e))?,
tcp_stream,
);
info!("Starting mTLS handshake for {}", address);
while tls_stream.conn.is_handshaking() {
tls_stream
.conn
.complete_io(&mut tls_stream.sock)
.map_err(|e| format!("TLS handshake failed: {}", e))?;
}
info!("mTLS handshake completed successfully for {}", address);
Ok(TransportStream::Mtls(tls_stream))
}

228
src/tcp/tls/enrollment.rs Normal file
View File

@@ -0,0 +1,228 @@
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256};
use log::info;
use reqwest::blocking::Client;
use serde::Deserialize;
use uuid::Uuid;
use super::connector::connect_mtls_from_pem;
use crate::tcp::transport::TransportStream;
#[derive(Deserialize)]
struct EnrollmentResponse {
#[serde(rename = "signedCert")]
signed_cert: String,
ca0: String,
}
struct EnrollmentConfig {
server_port: String,
enroll_path: String,
}
fn extract_tag_value(xml: &str, tag_name: &str) -> Option<String> {
let open_tag = format!("<{}>", tag_name);
let close_tag = format!("</{}>", tag_name);
let start = xml.find(&open_tag)? + open_tag.len();
let end = xml[start..].find(&close_tag)? + start;
Some(xml[start..end].trim().to_string())
}
fn wrap_pem_body(base64_body: &str, begin: &str, end: &str) -> String {
let mut wrapped = String::new();
let normalized = base64_body.trim().replace(['\r', '\n'], "");
wrapped.push_str(begin);
wrapped.push('\n');
for chunk in normalized.as_bytes().chunks(64) {
wrapped.push_str(std::str::from_utf8(chunk).unwrap_or_default());
wrapped.push('\n');
}
wrapped.push_str(end);
wrapped.push('\n');
wrapped
}
fn enrollment_http_client() -> Result<Client, String> {
Client::builder()
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| format!("failed to build enrollment HTTP client: {}", e))
}
fn response_error_details(response: reqwest::blocking::Response) -> String {
let status = response.status();
match response.text() {
Ok(body) => {
let trimmed = body.trim();
if trimmed.is_empty() {
status.to_string()
} else {
format!("{}: {}", status, trimmed)
}
}
Err(_) => status.to_string(),
}
}
fn fetch_enrollment_config(host: &str, enroll_port: &str) -> Result<EnrollmentConfig, String> {
let url = format!(
"https://{}:{}/Marti/api/tls/config",
host.trim(),
enroll_port.trim()
);
info!("Fetching TAK enrollment config from {}", url);
let response = enrollment_http_client()?
.get(&url)
.send()
.map_err(|e| format!("failed to fetch {}: {}", url, e))?;
if !response.status().is_success() {
return Err(format!(
"failed to fetch {}: {}",
url,
response_error_details(response)
));
}
let response_text = response
.text()
.map_err(|e| format!("failed to read config response from {}: {}", url, e))?;
let server_port = extract_tag_value(&response_text, "serverPort")
.ok_or_else(|| "missing serverPort in /Marti/api/tls/config response".to_string())?;
let enroll_path = extract_tag_value(&response_text, "enrollPath")
.ok_or_else(|| "missing enrollPath in /Marti/api/tls/config response".to_string())?;
info!(
"Enrollment config received: server_port={} enroll_path={}",
server_port, enroll_path
);
Ok(EnrollmentConfig {
server_port,
enroll_path,
})
}
fn enroll_client_certificate(
host: &str,
enroll_port: &str,
enroll_path: &str,
username: &str,
password: &str,
client_uid: &str,
) -> Result<(String, String, String), String> {
info!(
"Generating RSA client keypair and CSR for enrolled TAK client {}",
client_uid
);
let key_pair = KeyPair::generate_for(&PKCS_RSA_SHA256)
.map_err(|e| format!("failed to generate client keypair: {}", e))?;
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(DnType::CommonName, client_uid);
distinguished_name.push(DnType::OrganizationName, "ArmaTAK");
distinguished_name.push(DnType::OrganizationalUnitName, "ArmaTAK Session");
let mut params = CertificateParams::new(vec![])
.map_err(|e| format!("failed to create CSR params: {}", e))?;
params.distinguished_name = distinguished_name;
let csr = params
.serialize_request(&key_pair)
.map_err(|e| format!("failed to generate CSR: {}", e))?;
let csr_der = csr.der().as_ref().to_vec();
let url = format!(
"https://{}:{}{}?clientUid={}",
host.trim(),
enroll_port.trim(),
enroll_path.trim(),
client_uid.trim()
);
info!(
"Submitting client certificate enrollment request for {} to {}",
client_uid, url
);
let response = enrollment_http_client()?
.post(&url)
.basic_auth(username.trim(), Some(password.to_string()))
.header("Accept", "application/json")
.header("Content-Type", "application/pkcs10")
.body(csr_der)
.send()
.map_err(|e| format!("failed to enroll client certificate at {}: {}", url, e))?;
if !response.status().is_success() {
return Err(format!(
"failed to enroll client certificate at {}: {}",
url,
response_error_details(response)
));
}
let enrollment: EnrollmentResponse = response
.json()
.map_err(|e| format!("failed to parse enrollment response: {}", e))?;
info!(
"Enrollment response parsed successfully for {} (signed_cert_len={}, ca_len={})",
client_uid,
enrollment.signed_cert.len(),
enrollment.ca0.len()
);
let cert_pem = wrap_pem_body(
&enrollment.signed_cert,
"-----BEGIN CERTIFICATE-----",
"-----END CERTIFICATE-----",
);
let key_pem = key_pair.serialize_pem();
Ok((enrollment.ca0, cert_pem, key_pem))
}
pub fn enroll_and_connect(
host: &str,
server_name: &str,
enroll_port: &str,
username: &str,
password: &str,
client_uid: &str,
) -> Result<TransportStream, String> {
let normalized_client_uid = if client_uid.trim().is_empty() {
format!("armatak-{}", Uuid::new_v4())
} else {
client_uid.trim().to_string()
};
info!(
"Starting enroll_and_connect for host={} enroll_port={} server_name={} client_uid={}",
host,
enroll_port,
server_name,
normalized_client_uid
);
let enrollment_config = fetch_enrollment_config(host, enroll_port)?;
let (ca_cert_pem, client_cert_pem, client_key_pem) = enroll_client_certificate(
host,
enroll_port,
&enrollment_config.enroll_path,
username,
password,
&normalized_client_uid,
)?;
connect_mtls_from_pem(
&format!("{}:{}", host.trim(), enrollment_config.server_port.trim()),
if server_name.trim().is_empty() {
host.trim()
} else {
server_name.trim()
},
&ca_cert_pem,
&client_cert_pem,
&client_key_pem,
)
}

5
src/tcp/tls/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
mod connector;
mod enrollment;
pub use connector::connect_mtls;
pub use enrollment::enroll_and_connect;

90
src/tcp/transport.rs Normal file
View File

@@ -0,0 +1,90 @@
use log::info;
use rustls::{ClientConnection, StreamOwned};
use std::io::Write;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::time::Duration;
use super::config::ConnectionConfig;
use super::tls::{connect_mtls, enroll_and_connect};
const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const SOCKET_IO_TIMEOUT: Duration = Duration::from_secs(10);
pub enum TransportStream {
Plain(TcpStream),
Mtls(StreamOwned<ClientConnection, TcpStream>),
}
impl TransportStream {
pub fn write_message(&mut self, message: &[u8]) -> Result<(), std::io::Error> {
match self {
Self::Plain(stream) => {
stream.write_all(message)?;
stream.flush()
}
Self::Mtls(stream) => {
stream.write_all(message)?;
stream.flush()
}
}
}
}
fn connect_plain(address: &str) -> Result<TransportStream, String> {
let socket_addr: SocketAddr = address
.to_socket_addrs()
.map_err(|e| format!("failed to resolve {}: {}", address, e))?
.next()
.ok_or_else(|| format!("failed to resolve {}: no socket addresses returned", address))?;
info!(
"Opening plain TCP connection to {} (resolved={}) with timeout {:?}",
address, socket_addr, TCP_CONNECT_TIMEOUT
);
let stream = TcpStream::connect_timeout(&socket_addr, TCP_CONNECT_TIMEOUT)
.map_err(|e| format!("failed to connect to {}: {}", address, e))?;
stream
.set_read_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set read timeout on {}: {}", address, e))?;
stream
.set_write_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set write timeout on {}: {}", address, e))?;
Ok(TransportStream::Plain(stream))
}
pub fn connect_stream(config: &ConnectionConfig) -> Result<TransportStream, String> {
info!("connect_stream invoked for {}", config.describe());
match config {
ConnectionConfig::Plain { address } => connect_plain(address),
ConnectionConfig::Mtls {
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
} => connect_mtls(
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
),
ConnectionConfig::EnrollMtls {
host,
server_name,
enroll_port,
username,
password,
client_uid,
} => enroll_and_connect(
host,
server_name,
enroll_port,
username,
password,
client_uid,
),
}
}

View File

@@ -1,25 +1,25 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::init; use crate::init;
use uuid::Uuid; use std::vec;
use std::vec; use uuid::Uuid;
#[test] #[test]
fn uuid_output_is_uuid4_identifier() { fn uuid_output_is_uuid4_identifier() {
let extension = init().testing(); let extension = init().testing();
let (output, _) = extension.call("uuid", None); let (output, _) = extension.call("uuid", None);
let validation = Uuid::parse_str(&output); let validation = Uuid::parse_str(&output);
assert!(validation.is_ok()) assert!(validation.is_ok())
} }
#[test] #[test]
fn uuid_output_throws_if_passed_args() { fn uuid_output_throws_if_passed_args() {
let extension = init().testing(); let extension = init().testing();
let args: Vec<String> = vec![1.to_string(),2.to_string()]; let args: Vec<String> = vec![1.to_string(), 2.to_string()];
let (output, _) = extension.call("uuid", Some(args)); let (output, _) = extension.call("uuid", Some(args));
assert_eq!(output,"") assert_eq!(output, "")
} }
} }

View File

@@ -9,116 +9,119 @@ use std::thread;
use crate::cot; use crate::cot;
pub enum UdpCommand { pub enum UdpCommand {
SendMessage(String, Context), SendMessage(String, Context),
Stop, Stop,
} }
pub struct UdpClient { pub struct UdpClient {
pub(crate) tx: Sender<UdpCommand>, pub(crate) tx: Sender<UdpCommand>,
} }
impl UdpClient { impl UdpClient {
pub fn start(&self, address: String, rx: Receiver<UdpCommand>, ctx: Context) { pub fn start(&self, address: String, rx: Receiver<UdpCommand>, ctx: Context) {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() { if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.stop(); client.stop();
}
thread::spawn(move || {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(e) => {
let _ = ctx.callback_data(
"UDP SOCKET ERROR",
"Failed to bind UDP socket",
e.to_string(),
);
info!("Failed to bind UDP socket: {}", e);
return;
}
};
let _ = ctx.callback_data("UDP SOCKET", "EUD Connected", address.clone());
let mut running = true;
while running {
match rx.recv() {
Ok(UdpCommand::SendMessage(message, context)) => {
if let Err(e) = socket.send_to(message.as_bytes(), &address) {
info!("Failed to send UDP message: {}", e);
let _ = context.callback_data(
"UDP SOCKET ERROR",
"Failed to send UDP message",
e.to_string(),
);
}
}
Ok(UdpCommand::Stop) => {
running = false;
info!("Stopping UDP client.");
}
Err(error) => {
info!("Error receiving command: {}", error.to_string());
}
}
}
});
} }
thread::spawn(move || { pub fn send_payload(&self, context: Context, payload: String) {
let socket = match UdpSocket::bind("0.0.0.0:0") { let tx = self.tx.clone();
Ok(s) => s, thread::spawn(move || {
Err(e) => { tx.send(UdpCommand::SendMessage(payload, context)).unwrap();
let _ = ctx.callback_data( });
"UDP SOCKET ERROR", }
"Failed to bind UDP socket",
e.to_string(),
);
info!("Failed to bind UDP socket: {}", e);
return;
}
};
let _ = ctx.callback_data("UDP SOCKET", "EUD Connected", address.clone()); pub fn stop(&self) {
let tx = self.tx.clone();
let mut running = true; thread::spawn(move || {
while running { tx.send(UdpCommand::Stop).unwrap();
match rx.recv() { });
Ok(UdpCommand::SendMessage(message, context)) => { }
if let Err(e) = socket.send_to(message.as_bytes(), &address) {
info!("Failed to send UDP message: {}", e);
let _ = context.callback_data(
"UDP SOCKET ERROR",
"Failed to send UDP message",
e.to_string(),
);
}
}
Ok(UdpCommand::Stop) => {
running = false;
info!("Stopping UDP client.");
}
Err(error) => {
info!("Error receiving command: {}", error.to_string());
}
}
}
});
}
pub fn send_payload(&self, context: Context, payload: String) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(UdpCommand::SendMessage(payload, context)).unwrap();
});
}
pub fn stop(&self) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(UdpCommand::Stop).unwrap();
});
}
} }
lazy_static! { lazy_static! {
static ref UDP_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None)); static ref UDP_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None));
} }
pub fn start(ctx: Context, address: String) -> &'static str { pub fn start(ctx: Context, address: String) -> &'static str {
let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel(); let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel();
let client = UdpClient { tx }; let client = UdpClient { tx };
client.start(address, rx, ctx); client.start(address, rx, ctx);
let mut client_guard = UDP_CLIENT.lock().unwrap(); let mut client_guard = UDP_CLIENT.lock().unwrap();
*client_guard = Some(client); *client_guard = Some(client);
"Starting UDP Client" "Starting UDP Client"
} }
pub fn send_payload(ctx: Context, payload: String) -> &'static str { pub fn send_payload(ctx: Context, payload: String) -> &'static str {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() { if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.send_payload(ctx, payload); client.send_payload(ctx, payload);
} else { } else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running"); let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
} }
"Sending payload to UDP server" "Sending payload to UDP server"
} }
pub fn send_gps_cot(ctx: Context, cursor_over_time: cot::gps::ExternalPositionPayload) -> &'static str { pub fn send_gps_cot(
let payload = cursor_over_time.to_cot().convert_to_xml(); ctx: Context,
send_payload(ctx, payload); cursor_over_time: cot::gps::ExternalPositionPayload,
) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending GPS Cursor Over Time to UDP server" "Sending GPS Cursor Over Time to UDP server"
} }
pub fn stop(ctx: Context) -> &'static str { pub fn stop(ctx: Context) -> &'static str {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() { if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.stop(); client.stop();
let _ = ctx.callback_null("UDP SOCKET", "EUD Disconnected"); let _ = ctx.callback_null("UDP SOCKET", "EUD Disconnected");
} else { } else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running"); let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
} }
"Stopping UDP Client" "Stopping UDP Client"
} }

View File

@@ -1,23 +1,23 @@
use std::net::{IpAddr, UdpSocket}; use std::net::{IpAddr, UdpSocket};
pub fn get_local_address() -> String { pub fn get_local_address() -> String {
fn get_local_ip() -> Result<IpAddr, String> { fn get_local_ip() -> Result<IpAddr, String> {
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| e.to_string())?; let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| e.to_string())?;
socket.connect("8.8.8.8:80").map_err(|e| e.to_string())?; socket.connect("8.8.8.8:80").map_err(|e| e.to_string())?;
socket socket
.local_addr() .local_addr()
.map(|addr| addr.ip()) .map(|addr| addr.ip())
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
} }
let parsed_data = get_local_ip(); let parsed_data = get_local_ip();
match parsed_data { match parsed_data {
Ok(ip) => { Ok(ip) => {
return format!("ws://{}:4152", ip.to_string()); return format!("ws://{}:4152", ip.to_string());
} }
Err(_) => { Err(_) => {
return "not provided".to_string(); return "not provided".to_string();
} }
} }
} }

View File

@@ -2,11 +2,11 @@ use crate::structs::LogPayload;
use log::{error, info, warn}; use log::{error, info, warn};
pub fn log_info(data: LogPayload) -> String { pub fn log_info(data: LogPayload) -> String {
match data.status.as_str() { match data.status.as_str() {
"info" => info!("{}", data.message), "info" => info!("{}", data.message),
"warn" => warn!("{}", data.message), "warn" => warn!("{}", data.message),
"error" => error!("{}", data.message), "error" => error!("{}", data.message),
_ => error!("{}", "Wrong log call"), _ => error!("{}", "Wrong log call"),
} }
"logged".to_string() "logged".to_string()
} }

View File

@@ -1,3 +1,3 @@
pub mod uuid;
pub mod address; pub mod address;
pub mod log; pub mod log;
pub mod uuid;

View File

@@ -1,7 +1,7 @@
pub fn get_uuid() -> String { pub fn get_uuid() -> String {
use uuid::Uuid; use uuid::Uuid;
let id = Uuid::new_v4().to_string(); let id = Uuid::new_v4().to_string();
return id; return id;
} }

View File

@@ -16,7 +16,13 @@ lazy_static! {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
fn build_rtsp_url(address: &str, port: &str, stream_path: &str, username: &str, password: &str) -> String { fn build_rtsp_url(
address: &str,
port: &str,
stream_path: &str,
username: &str,
password: &str,
) -> String {
if username.is_empty() || password.is_empty() { if username.is_empty() || password.is_empty() {
format!("rtsp://{}:{}/{}", address, port, stream_path) format!("rtsp://{}:{}/{}", address, port, stream_path)
} else { } else {
@@ -28,20 +34,22 @@ fn build_rtsp_url(address: &str, port: &str, stream_path: &str, username: &str,
} }
#[cfg(any(target_os = "windows", target_os = "linux"))] #[cfg(any(target_os = "windows", target_os = "linux"))]
fn spawn_ffmpeg( fn spawn_ffmpeg(rtsp_url: String, stop_rx: Receiver<()>, status_tx: Sender<Result<(), String>>) {
rtsp_url: String,
stop_rx: Receiver<()>,
status_tx: Sender<Result<(), String>>,
) {
thread::spawn(move || { thread::spawn(move || {
let mut cmd = Command::new("ffmpeg"); let mut cmd = Command::new("ffmpeg");
cmd.args(&[ cmd.args(&[
"-f","x11grab", "-f",
"-framerate","30", "x11grab",
"-video_size","1920x1080", "-framerate",
"-i" ,":0", "30",
"-f","rtsp", "-video_size",
"-rtsp_transport","tcp", "1920x1080",
"-i",
":0",
"-f",
"rtsp",
"-rtsp_transport",
"tcp",
&rtsp_url, &rtsp_url,
]); ]);

1
vendor/arma-rs-proc/.cargo-ok vendored Normal file
View File

@@ -0,0 +1 @@
{"v":1}

View File

@@ -0,0 +1,6 @@
{
"git": {
"sha1": "adfc323899e58f20c05ebb37595d5ca4fd09367f"
},
"path_in_vcs": "arma-rs-proc"
}

42
vendor/arma-rs-proc/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,42 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "arma-rs-proc"
version = "1.11.1"
authors = ["Brett Mayson"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "proc macros for arma-rs"
readme = false
keywords = ["arma"]
license = "MIT"
repository = "https://github.com/brettmayson/arma-rs"
[lib]
name = "arma_rs_proc"
path = "src/lib.rs"
proc-macro = true
[dependencies.proc-macro2]
version = "1.0.92"
[dependencies.quote]
version = "1.0.37"
[dependencies.syn]
version = "2.0.90"
features = ["full"]

17
vendor/arma-rs-proc/Cargo.toml.orig generated vendored Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "arma-rs-proc"
description = "proc macros for arma-rs"
version = "1.11.1"
edition = "2021"
authors = ["Brett Mayson"]
repository = "https://github.com/brettmayson/arma-rs"
license = "MIT"
keywords = ["arma"]
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.92"
quote = "1.0.37"
syn = { version = "2.0.90", features = ["full"] }

View File

@@ -0,0 +1,145 @@
use syn::{Error, Result};
use crate::derive::CombinedErrors;
pub struct ContainerAttributes {
pub transparent: Attribute<bool>,
pub default: Attribute<bool>,
}
impl Default for ContainerAttributes {
fn default() -> Self {
Self {
transparent: Attribute::new(false),
default: Attribute::new(false),
}
}
}
impl ParseAttr for ContainerAttributes {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("transparent") {
return self.transparent.set(&meta, true);
}
if meta.path.is_ident("default") {
return self.default.set(&meta, true);
}
Err(meta.error(format!(
"unknown arma container attribute `{}`",
path_to_string(&meta.path)
)))
}
}
pub struct FieldAttributes {
pub default: Attribute<bool>,
pub from_str: Attribute<bool>,
pub to_string: Attribute<bool>,
}
impl Default for FieldAttributes {
fn default() -> Self {
Self {
default: Attribute::new(false),
from_str: Attribute::new(false),
to_string: Attribute::new(false),
}
}
}
impl ParseAttr for FieldAttributes {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("default") {
return self.default.set(&meta, true);
}
if meta.path.is_ident("from_str") {
return self.from_str.set(&meta, true);
}
if meta.path.is_ident("to_string") {
return self.to_string.set(&meta, true);
}
Err(meta.error(format!(
"unknown arma field attribute `{}`",
path_to_string(&meta.path)
)))
}
}
pub trait ParseAttr {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()>;
}
pub fn parse_attributes<T>(errors: &mut CombinedErrors, attrs: &[syn::Attribute]) -> T
where
T: ParseAttr + Default + Sized,
{
attrs.iter().fold(T::default(), |mut attributes, attr| {
if !attr.path().is_ident("arma") {
return attributes;
}
let result = attr.parse_nested_meta(|meta| {
if let Err(err) = attributes.parse_attr(meta) {
errors.add(err);
}
Ok(())
});
if let Err(err) = result {
errors.add(err);
}
attributes
})
}
pub struct Attribute<T> {
value: T,
path: Option<syn::Path>,
}
impl<T> Attribute<T> {
fn new(default: T) -> Self {
Self {
value: default,
path: None,
}
}
fn set(&mut self, meta: &syn::meta::ParseNestedMeta, value: T) -> Result<()> {
if self.is_set() {
return Err(meta.error(format!(
"duplicate arma attribute `{}`",
path_to_string(&meta.path)
)));
}
self.value = value;
self.path = Some(meta.path.clone());
Ok(())
}
pub fn is_set(&self) -> bool {
self.path.is_some()
}
pub fn value(&self) -> &T {
&self.value
}
#[must_use]
pub fn error(&self, message: &str) -> Error {
Error::new_spanned(self.path.as_ref().unwrap(), message)
}
}
fn path_to_string(path: &syn::Path) -> String {
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}

124
vendor/arma-rs-proc/src/derive/data.rs vendored Normal file
View File

@@ -0,0 +1,124 @@
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{Error, Result};
use crate::derive::{
attributes::{parse_attributes, ContainerAttributes, FieldAttributes},
r#struct, CombinedErrors,
};
pub struct ContainerData {
pub attributes: ContainerAttributes,
pub ident: syn::Ident,
pub generics: syn::Generics,
pub data: Data,
}
pub enum Data {
Struct(StructData),
}
pub enum StructData {
Map(Vec<FieldNamed>),
Tuple(Vec<FieldUnnamed>),
NewType(FieldUnnamed),
}
impl ContainerData {
pub fn from_input(errors: &mut CombinedErrors, input: syn::DeriveInput) -> Result<Self> {
let data = match input.data {
syn::Data::Struct(data) => Data::Struct(StructData::new(errors, data)?),
syn::Data::Enum(_) => Err(Error::new(Span::call_site(), "enums aren't supported"))?,
syn::Data::Union(_) => Err(Error::new(Span::call_site(), "unions aren't supported"))?,
};
let attributes = parse_attributes::<ContainerAttributes>(errors, &input.attrs);
Ok(Self {
attributes,
ident: input.ident,
generics: input.generics,
data,
})
}
pub fn validate_attributes(&self, errors: &mut CombinedErrors) {
match self.data {
Data::Struct(ref data) => {
r#struct::validate_attributes(errors, &self.attributes, data);
}
}
}
pub fn impl_into_arma(&self) -> TokenStream {
match self.data {
Data::Struct(ref data) => r#struct::impl_into_arma(&self.attributes, data),
}
}
pub fn impl_from_arma(&self) -> TokenStream {
match self.data {
Data::Struct(ref data) => r#struct::impl_from_arma(&self.attributes, data),
}
}
}
pub struct FieldNamed {
pub attributes: FieldAttributes,
pub ident: syn::Ident,
pub name: String,
pub _ty: syn::Type,
}
pub struct FieldUnnamed {
pub attributes: FieldAttributes,
pub index: syn::Index,
pub ty: syn::Type,
}
impl FieldNamed {
pub fn new(errors: &mut CombinedErrors, field: syn::Field) -> Self {
let ident = field.ident.unwrap();
let name = ident.to_string();
Self {
attributes: parse_attributes::<FieldAttributes>(errors, &field.attrs),
ident,
name,
_ty: field.ty,
}
}
}
impl FieldUnnamed {
pub fn new(errors: &mut CombinedErrors, field: syn::Field, index: usize) -> Self {
Self {
attributes: parse_attributes::<FieldAttributes>(errors, &field.attrs),
index: syn::Index::from(index),
ty: field.ty,
}
}
}
pub trait Field {
fn attributes(&self) -> &FieldAttributes;
fn token(&self) -> TokenStream;
}
impl Field for FieldNamed {
fn attributes(&self) -> &FieldAttributes {
&self.attributes
}
fn token(&self) -> TokenStream {
self.ident.to_token_stream()
}
}
impl Field for FieldUnnamed {
fn attributes(&self) -> &FieldAttributes {
&self.attributes
}
fn token(&self) -> TokenStream {
self.index.to_token_stream()
}
}

72
vendor/arma-rs-proc/src/derive/mod.rs vendored Normal file
View File

@@ -0,0 +1,72 @@
mod attributes;
mod data;
mod r#struct;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Result};
use data::ContainerData;
pub fn generate_from_arma(input: DeriveInput) -> Result<TokenStream> {
let container = parse_container_data(input)?;
let body = container.impl_from_arma();
let ident = container.ident;
let (impl_generics, ty_generics, where_clause) = container.generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics arma_rs::FromArma for #ident #ty_generics #where_clause {
fn from_arma(func_input: String) -> Result<Self, arma_rs::FromArmaError> {
#body
}
}
})
}
pub fn generate_into_arma(input: DeriveInput) -> Result<TokenStream> {
let container = parse_container_data(input)?;
let body = container.impl_into_arma();
let ident = container.ident;
let (impl_generics, ty_generics, where_clause) = container.generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics arma_rs::IntoArma for #ident #ty_generics #where_clause {
fn to_arma(&self) -> arma_rs::Value {
#body
}
}
})
}
fn parse_container_data(input: DeriveInput) -> Result<ContainerData> {
let mut errors = CombinedErrors::new();
let container = ContainerData::from_input(&mut errors, input)?;
container.validate_attributes(&mut errors);
errors.into_result().and(Ok(container))
}
pub struct CombinedErrors {
root: Option<syn::Error>,
}
impl CombinedErrors {
fn new() -> Self {
Self { root: None }
}
pub fn add(&mut self, error: syn::Error) {
match &mut self.root {
Some(root) => root.combine(error),
None => self.root = Some(error),
}
}
pub fn into_result(self) -> Result<()> {
match self.root {
Some(error) => Err(error),
None => Ok(()),
}
}
}

View File

@@ -0,0 +1,162 @@
use proc_macro2::TokenStream;
use quote::quote;
use crate::derive::{
attributes::ContainerAttributes,
data::{Field, FieldNamed, FieldUnnamed, StructData},
};
pub fn impl_from_arma(attributes: &ContainerAttributes, data: &StructData) -> TokenStream {
// For simplicity sake we assume that theres no conflicts and everything has already been validated
match &data {
StructData::Map(fields) => map_struct(attributes, fields),
StructData::Tuple(fields) => tuple_struct(attributes, fields),
StructData::NewType(field) => newtype_struct(attributes, field),
}
}
fn map_struct(attributes: &ContainerAttributes, fields: &[FieldNamed]) -> TokenStream {
if *attributes.transparent.value() {
return newtype_struct(attributes, fields.first().unwrap());
}
let mut setup = TokenStream::new();
setup.extend(quote! {
let mut input_as_values = std::collections::HashMap::<String, arma_rs::Value>::default();
let input_pairs: Vec<(String, arma_rs::Value)> = FromArma::from_arma(func_input)?;
for (k, v) in input_pairs {
if input_as_values.insert(k.clone(), v).is_some() {
return Err(arma_rs::FromArmaError::DuplicateField(k));
}
}
});
if *attributes.default.value() {
setup.extend(quote! {
let container_default: Self = std::default::Default::default();
});
};
let field_bodies = fields.iter().map(|field| {
let (ident, name) = (&field.ident, &field.name);
let some_match = if *field.attributes.from_str.value() {
quote!(input_value
.to_string()
.parse()
.map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(input_value.to_string())?)
};
let none_match = if *field.attributes.default.value() {
quote!(std::default::Default::default())
} else if *attributes.default.value() {
quote!(container_default.#ident)
} else {
quote!(return Err(arma_rs::FromArmaError::MissingField(#name.to_string())))
};
quote! {
#ident: match input_as_values.remove(#name) {
Some(input_value) => #some_match,
None => #none_match,
}
}
});
let check_unknown = quote! {
if let Some(unknown) = input_as_values.keys().next() {
return Err(arma_rs::FromArmaError::UnknownField(unknown.clone()));
}
};
quote! {
#setup
let result = Self {
#(#field_bodies),*
};
#check_unknown
Ok(result)
}
}
fn tuple_struct(attributes: &ContainerAttributes, fields: &[FieldUnnamed]) -> TokenStream {
let mut setup = TokenStream::new();
setup.extend(quote! {
let input_as_values: Vec<arma_rs::Value> = arma_rs::FromArma::from_arma(func_input)?;
let mut input_as_values = input_as_values.into_iter();
});
if *attributes.default.value() {
setup.extend(quote! {
let container_default: Self = std::default::Default::default();
});
};
let expected_len = fields.len();
let field_bodies = fields.iter().map(|field| {
let index = &field.index;
let some_match = if *field.attributes.from_str.value() {
quote!(input_value
.to_string()
.parse()
.map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(input_value.to_string())?)
};
let none_match = if *field.attributes.default.value() {
quote!(std::default::Default::default())
} else if *attributes.default.value() {
quote!(container_default.#index)
} else {
quote!(return Err(arma_rs::FromArmaError::InvalidLength {
expected: #expected_len,
actual: #index,
}))
};
quote! {
match input_as_values.next() {
Some(input_value) => #some_match,
None => #none_match,
}
}
});
let check_unknown = quote! {
let remaining = input_as_values.len();
if remaining > 0 {
return Err(arma_rs::FromArmaError::InvalidLength {
expected: #expected_len,
actual: #expected_len + remaining,
});
}
};
quote! {
#setup
let result = Self (
#(#field_bodies),*
);
#check_unknown
Ok(result)
}
}
fn newtype_struct(_attributes: &ContainerAttributes, field: &impl Field) -> TokenStream {
let token = field.token();
let field_body = if *field.attributes().from_str.value() {
quote!(func_input.parse().map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(func_input)?)
};
quote! {
Ok(Self {
#token: #field_body
})
}
}

View File

@@ -0,0 +1,72 @@
use proc_macro2::TokenStream;
use quote::quote;
use crate::derive::{
attributes::ContainerAttributes,
data::{Field, FieldNamed, FieldUnnamed, StructData},
};
pub fn impl_into_arma(attributes: &ContainerAttributes, data: &StructData) -> TokenStream {
// For simplicity sake we assume that theres no conflicts and everything has already been validated
match &data {
StructData::Map(fields) => map_struct(attributes, fields),
StructData::Tuple(fields) => tuple_struct(attributes, fields),
StructData::NewType(field) => newtype_struct(attributes, field),
}
}
fn map_struct(attributes: &ContainerAttributes, fields: &[FieldNamed]) -> TokenStream {
if *attributes.transparent.value() {
return newtype_struct(attributes, fields.first().unwrap());
}
let field_bodies = fields.iter().map(|field| {
let (ident, name) = (&field.ident, &field.name);
let (key, value) = if *field.attributes.to_string.value() {
(quote!(#name.to_string()), quote!(self.#ident.to_string()))
} else {
(quote!(#name.to_string()), quote!(self.#ident))
};
quote!((#key, arma_rs::IntoArma::to_arma(&#value)))
});
quote! {
std::collections::HashMap::<String, arma_rs::Value>::from([
#(#field_bodies),*
]).to_arma()
}
}
fn tuple_struct(_attributes: &ContainerAttributes, fields: &[FieldUnnamed]) -> TokenStream {
let field_bodies = fields.iter().map(|field| {
let index = &field.index;
if *field.attributes.to_string.value() {
quote!(self.#index.to_string())
} else {
quote!(self.#index)
}
});
quote! {
Vec::<arma_rs::Value>::from([
#(arma_rs::IntoArma::to_arma(&#field_bodies)),*
]).to_arma()
}
}
fn newtype_struct(_attributes: &ContainerAttributes, field: &impl Field) -> TokenStream {
let token = field.token();
let field_body = if *field.attributes().to_string.value() {
quote!(self.#token.to_string())
} else {
quote!(self.#token)
};
quote! {
arma_rs::IntoArma::to_arma(&#field_body)
}
}

View File

@@ -0,0 +1,62 @@
mod from;
mod into;
mod validate;
use proc_macro2::Span;
use syn::{Error, Result};
pub use from::impl_from_arma;
pub use into::impl_into_arma;
pub use validate::validate_attributes;
use crate::derive::{
data::{FieldNamed, FieldUnnamed, StructData},
CombinedErrors,
};
impl StructData {
pub fn new(errors: &mut CombinedErrors, data: syn::DataStruct) -> Result<Self> {
match data.fields {
syn::Fields::Unit => Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
)),
syn::Fields::Named(fields) => {
if fields.named.is_empty() {
return Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
));
}
let fields = fields
.named
.into_iter()
.map(|f| FieldNamed::new(errors, f))
.collect::<_>();
Ok(Self::Map(fields))
}
syn::Fields::Unnamed(fields) => {
if fields.unnamed.is_empty() {
return Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
));
}
if fields.unnamed.len() == 1 {
let field = FieldUnnamed::new(errors, fields.unnamed[0].clone(), 0);
Ok(Self::NewType(field))
} else {
let fields = fields
.unnamed
.into_iter()
.enumerate()
.map(|(i, f)| FieldUnnamed::new(errors, f, i))
.collect::<_>();
Ok(Self::Tuple(fields))
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
use syn::Error;
use crate::derive::{
attributes::{Attribute, ContainerAttributes, FieldAttributes},
data::StructData,
CombinedErrors,
};
pub fn validate_attributes(
errors: &mut CombinedErrors,
attributes: &ContainerAttributes,
data: &StructData,
) {
if *attributes.transparent.value() {
match data {
StructData::Map(fields) if fields.len() > 1 => {
errors.add(
attributes
.transparent
.error("#[arma(transparent)] structs must have exactly one field"),
);
}
StructData::Tuple(_) => {
errors.add(
attributes
.transparent
.error("#[arma(transparent)] cannot be used on tuple like structs"),
);
}
_ => {}
}
}
if let Some(attr) = get_default_attr(attributes, data) {
match data {
StructData::Map(_) if *attributes.transparent.value() => {
errors.add(
attr.error("#[arma(default)] and #[arma(transparent)] cannot be used together"),
);
}
StructData::NewType(_) => {
errors.add(attr.error("#[arma(default)] cannot be used on new type structs"));
}
_ => {}
}
}
if let StructData::Tuple(fields) = data {
let mut index_first_default = None;
for (index, field) in fields.iter().enumerate() {
match index_first_default {
None => {
if field.attributes.default.is_set() {
index_first_default = Some(index);
}
}
Some(index) => {
if !field.attributes.default.is_set() {
errors.add(Error::new_spanned(&field.ty,
format!("field must have #[arma(default)] because previous field {} has #[arma(default)]", index)
));
}
}
}
}
}
}
fn get_default_attr<'a>(
attributes: &'a ContainerAttributes,
data: &'a StructData,
) -> Option<&'a Attribute<bool>> {
if *attributes.default.value() {
return Some(&attributes.default);
}
field_attributes(data)
.iter()
.find(|attr| *attr.default.value())
.map(|f| &f.default)
}
fn field_attributes(data: &StructData) -> Vec<&FieldAttributes> {
match data {
StructData::Map(fields) => fields.iter().map(|f| &f.attributes).collect(),
StructData::Tuple(fields) => fields.iter().map(|f| &f.attributes).collect(),
StructData::NewType(field) => vec![&field.attributes],
}
}

159
vendor/arma-rs-proc/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,159 @@
mod derive;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{DeriveInput, Error, ItemFn};
#[proc_macro_attribute]
/// Used to generate the necessary boilerplate for an Arma extension.
/// It should be applied to a function that takes no arguments and returns an extension.
pub fn arma(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(item as ItemFn);
let init = ast.sig.ident.clone();
let extern_type = if cfg!(windows) { "stdcall" } else { "C" };
let ext_init = quote! {
if RV_EXTENSION.is_none() {
RV_EXTENSION = Some(#init());
}
};
#[cfg(all(target_os = "windows", target_arch = "x86"))]
let prefix = "safe32_";
#[cfg(not(all(target_os = "windows", target_arch = "x86")))]
let prefix = "";
macro_rules! fn_ident {
( $name:literal ) => {
Ident::new(&format!("{prefix}{}", $name), Span::call_site())
};
}
let versionfn = fn_ident!("RVExtensionVersion");
let noargfn = fn_ident!("RVExtension");
let argfn = fn_ident!("RVExtensionArgs");
let callbackfn = fn_ident!("RVExtensionRegisterCallback");
let contextfn = fn_ident!("RVExtensionContext");
TokenStream::from(quote! {
use arma_rs::libc as arma_rs_libc;
static mut RV_EXTENSION: Option<Extension> = None;
#[cfg(all(target_os="windows", target_arch="x86"))]
arma_rs::link_args::windows! {
unsafe {
raw("/EXPORT:_RVExtensionVersion@8=_safe32_RVExtensionVersion@8");
raw("/EXPORT:_RVExtension@12=_safe32_RVExtension@12");
raw("/EXPORT:_RVExtensionArgs@20=_safe32_RVExtensionArgs@20");
raw("/EXPORT:_RVExtensionRegisterCallback@4=_safe32_RVExtensionRegisterCallback@4");
raw("/EXPORT:_RVExtensionContext@8=_safe32_RVExtensionContext@8");
}
}
/// Returns extension version, called by Arma on extension load.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #versionfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t) -> arma_rs_libc::c_int {
#ext_init
if let Some(ext) = &RV_EXTENSION {
arma_rs::write_cstr(ext.version().to_string(), output, size);
}
0
}
/// Run extension function, called by Arma on `callExtension` without arguments.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #noargfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t, function: *mut arma_rs_libc::c_char) {
#ext_init
if let Some(ext) = &RV_EXTENSION {
if ext.allow_no_args() {
ext.handle_call(function, output, size, None, None, true);
}
}
}
/// Run extension function with arguments, called by Arma on `callExtension` with arguments.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #argfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t, function: *mut arma_rs_libc::c_char, args: *mut *mut arma_rs_libc::c_char, arg_count: arma_rs_libc::c_int) -> arma_rs_libc::c_int {
#ext_init
if let Some(ext) = &RV_EXTENSION {
ext.handle_call(function, output, size, Some(args), Some(arg_count), true)
} else {
0
}
}
/// Set extension callback, called by Arma on extension load.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #callbackfn(callback: arma_rs::Callback) {
#ext_init
if let Some(ext) = &mut RV_EXTENSION {
ext.register_callback(callback);
ext.run_callbacks();
}
}
/// Provide extension call context, called by Arma on `callExtension`.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #contextfn(args: *mut *mut arma_rs_libc::c_char, arg_count: arma_rs_libc::c_int) {
#ext_init
if let Some(ext) = &mut RV_EXTENSION {
ext.handle_call_context(args, arg_count);
}
}
#ast
})
}
/// Derive implementation of `FromArma`, only supports structs.
/// - Map structs are converted from an hashmap.
/// - Tuple structs are converted from an array.
/// - Newtype structs directly use's the value's `FromArma` implementation.
/// - Unit-like structs are not supported.
///
/// ### Container Attributes
/// - `#[arma(transparent)]`: treat single field map structs as if its a newtype structs.
/// - `#[arma(default)]`: any missing field will be filled by the structs `Default` implementation.
///
/// ### Field Attributes
/// - `#[arma(from_str)]`: use the types `std::str::FromStr` instead of `FromArma`.
/// - `#[arma(default)]`: if missing use its `Default` implementation (takes precedence over container).
#[proc_macro_derive(FromArma, attributes(arma))]
pub fn derive_from_arma(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as DeriveInput);
derive::generate_from_arma(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}
/// Derive implementation of `IntoArma`, only supports structs.
/// - Map structs are converted to an hashmap.
/// - Tuple structs are converted to an array.
/// - Newtype structs directly use's the value's `IntoArma` implementation.
/// - Unit-like structs are not supported.
///
/// ### Container Attributes
/// - `#[arma(transparent)]`: treat single field map structs as if its a newtype structs.
///
/// ### Field Attributes
/// - `#[arma(to_string)]`: use the types `std::string::ToString` instead of `IntoArma`.
#[proc_macro_derive(IntoArma, attributes(arma))]
pub fn derive_into_arma(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as DeriveInput);
derive::generate_into_arma(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}

1
vendor/arma-rs/.cargo-ok vendored Normal file
View File

@@ -0,0 +1 @@
{"v":1}

6
vendor/arma-rs/.cargo_vcs_info.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"git": {
"sha1": "6cac89d7a9d02027bb85a6fa583b3ef0e8bf0f5a"
},
"path_in_vcs": "arma-rs"
}

951
vendor/arma-rs/Cargo.lock generated vendored Normal file
View File

@@ -0,0 +1,951 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "arma-rs"
version = "1.11.14"
dependencies = [
"arma-rs-proc",
"chrono",
"crossbeam-channel",
"libc",
"link_args",
"log",
"seq-macro",
"serde",
"serde_json",
"state",
"trybuild",
"uuid",
"winapi",
"windows 0.61.1",
]
[[package]]
name = "arma-rs-proc"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf67c0d0c7a59275e5ac4f3fce0cbdbcf3ba12e47bc30be6a3327d6a1bc151f8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "link_args"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7721e472624c9aaad27a5eb6b7c9c6045c7a396f2efb6dabaec1b640d5e89b"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"serde",
"serde_json",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "seq-macro"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "state"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
dependencies = [
"loom",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-triple"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790"
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "trybuild"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898"
dependencies = [
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [
"memchr",
]

108
vendor/arma-rs/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,108 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "arma-rs"
version = "1.11.14"
authors = ["Brett Mayson"]
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Arma 3 Extensions in Rust"
readme = "README.md"
keywords = ["arma"]
license = "MIT"
repository = "https://github.com/brettmayson/arma-rs"
[features]
default = ["extension"]
extension = [
"libc",
"crossbeam-channel",
]
[lib]
name = "arma_rs"
path = "src/lib.rs"
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
[[test]]
name = "derive"
path = "tests/derive.rs"
[[test]]
name = "emulate"
path = "tests/emulate.rs"
[[test]]
name = "main"
path = "tests/main.rs"
[dependencies.arma-rs-proc]
version = "1.11.1"
[dependencies.chrono]
version = "0.4.40"
optional = true
[dependencies.crossbeam-channel]
version = "0.5.14"
optional = true
[dependencies.libc]
version = "0.2.171"
optional = true
[dependencies.log]
version = "0.4.27"
[dependencies.seq-macro]
version = "0.3.6"
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
optional = true
[dependencies.serde_json]
version = "1.0.140"
optional = true
[dependencies.state]
version = "0.6.0"
[dependencies.uuid]
version = "1.16.0"
optional = true
[dev-dependencies.trybuild]
version = "1.0.104"
[target.'cfg(all(target_os="windows", target_arch="x86"))'.dependencies.link_args]
version = "0.6.0"
[target."cfg(windows)".dependencies.winapi]
version = "0.3.9"
features = ["libloaderapi"]
[target."cfg(windows)".dependencies.windows]
version = "0.61.1"
features = [
"Win32_Foundation",
"Win32_System_Console",
]

41
vendor/arma-rs/Cargo.toml.orig generated vendored Normal file
View File

@@ -0,0 +1,41 @@
[package]
name = "arma-rs"
description = "Arma 3 Extensions in Rust"
version = "1.11.14"
edition = "2021"
authors = ["Brett Mayson"]
repository = "https://github.com/brettmayson/arma-rs"
license = "MIT"
keywords = ["arma"]
readme = "../README.md"
[dependencies]
arma-rs-proc = { path = "../arma-rs-proc", version = "1.11.1" }
log = "0.4.27"
state = "0.6.0"
seq-macro = "0.3.6"
chrono = { version = "0.4.40", optional = true }
crossbeam-channel = { version = "0.5.14", optional = true }
libc = { version = "0.2.171", optional = true }
serde = { version = "1.0.219", features = ["derive"], optional = true }
serde_json = { version = "1.0.140", optional = true }
uuid = { version = "1.16.0", optional = true }
[target.'cfg(all(target_os="windows", target_arch="x86"))'.dependencies]
link_args = "0.6.0"
[target.'cfg(windows)'.dependencies.winapi]
version = "0.3.9"
features = ["libloaderapi"]
[target.'cfg(windows)'.dependencies.windows]
version = "0.61.1"
features = ["Win32_Foundation", "Win32_System_Console"]
[dev-dependencies]
trybuild = "1.0.104"
[features]
default = ["extension"]
extension = ["libc", "crossbeam-channel"]

439
vendor/arma-rs/README.md vendored Normal file
View File

@@ -0,0 +1,439 @@
# arma-rs
[Join the arma-rs Discord!](https://discord.gg/qXWUrrwy5d)
[![codecov](https://codecov.io/gh/BrettMayson/arma-rs/branch/main/graph/badge.svg?token=A1H7SEZ434)](https://codecov.io/gh/BrettMayson/arma-rs)
The best way to make Arma 3 Extensions.
## Usage
```toml
[dependencies]
arma-rs = "1.11.10"
[lib]
name = "my_extension"
crate-type = ["cdylib"]
```
### Hello World
```rust
use arma_rs::{arma, Extension};
#[arma]
fn init() -> Extension {
Extension::build()
.command("hello", hello)
.command("welcome", welcome)
.finish()
}
pub fn hello() -> &'static str {
"Hello"
}
pub fn welcome(name: String) -> String {
format!("Welcome {}", name)
}
```
```sqf
"my_extension" callExtension ["hello", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["welcome", ["John"]]; // Returns ["Welcome John", 0, 0]
```
## Command Groups
Commands can be grouped together, making your large projects much easier to manage.
```rust
use arma_rs::{arma, Extension, Group};
#[arma]
fn init() -> Extension {
Extension::build()
.group("hello",
Group::new()
.command("english", hello::english)
.group("english",
Group::new()
.command("casual", hello::english_casual)
)
.command("french", hello::french),
)
.group("welcome",
Group::new()
.command("english", welcome::english)
.command("french", welcome::french),
)
.finish()
}
mod hello {
pub fn english() -> &'static str {
"Hello"
}
pub fn english_casual() -> &'static str {
"Hey"
}
pub fn french() -> &'static str {
"Bonjour"
}
}
mod welcome {
pub fn english(name: String) -> String {
format!("Welcome {}", name)
}
pub fn french(name: String) -> String {
format!("Bienvenue {}", name)
}
}
```
Commands groups are called by using the format `group:command`. You can nest groups as much as you want.
```sqf
"my_extension" callExtension ["hello:english", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["hello:english:casual", []]; // Returns ["Hey", 0, 0]
"my_extension" callExtension ["hello:french", []]; // Returns ["Bonjour", 0, 0]
```
## Callbacks
Extension callbacks can be invoked anywhere in the extension by adding a variable of type `Context` to the start of a handler.
```rust
use arma_rs::Context;
pub fn sleep(ctx: Context, duration: u64, id: String) {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(duration));
ctx.callback_data("example_timer", "done", Some(id));
});
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new().command("sleep", sleep)
}
```
## Call Context
Since Arma v2.11 additional context is provided each time the extension is called. This context can be accessed through the optional `ArmaCallContext` argument.
Since Arma v2.18 the context is only requested from Arma when the functionh has `ArmaCallContext` as an argument.
```rust
use arma_rs::{CallContext, CallContextStackTrace};
pub fn call_context(call_context: CallContext) -> String {
format!(
"{:?},{:?},{:?},{:?},{:?}",
call_context.caller(),
call_context.source(),
call_context.mission(),
call_context.server(),
call_context.remote_exec_owner(),
)
}
pub fn stack_trace(call_context: CallContextStackTrace) -> String {
format!(
"{:?}\n{:?}",
call_context.source(),
call_context.stack_trace()
)
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new()
.command("call_context", call_context)
.command("stack_trace", stack_trace)
}
```
## Persistent State
Both the extension and command groups allow for type based persistent state values with at most one instance per type. These state values can then be accessed through the optional `Context` argument.
### Global State
Extension state is accessible from any command handler.
```rust
use arma_rs::{arma, Context, ContextState, Extension};
use std::sync::atomic::{AtomicU32, Ordering};
#[arma]
fn init() -> Extension {
Extension::build()
.command("counter_increment", increment)
.state(AtomicU32::new(0))
.finish()
}
pub fn increment(ctx: Context) -> Result<(), ()> {
let Some(counter) = ctx.global().get::<AtomicU32>() else {
return Err(());
};
counter.fetch_add(1, Ordering::SeqCst);
Ok(())
}
```
### Group State
Command group state is only accessible from command handlers within the same group.
```rust
use arma_rs::{Context, ContextState, Extension};
use std::sync::atomic::{AtomicU32, Ordering};
pub fn increment(ctx: Context) -> Result<(), ()> {
let Some(counter) = ctx.group().get::<AtomicU32>() else {
return Err(());
};
counter.fetch_add(1, Ordering::SeqCst);
Ok(())
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new()
.command("increment", increment)
.state(AtomicU32::new(0))
}
```
## Custom Types
If you're bringing your existing Rust library with your own types, you can easily define how they are converted to and from Arma.
```rust
use arma_rs::{FromArma, IntoArma, Value, FromArmaError};
pub struct MemoryReport {
total: u64,
free: u64,
avail: u64,
}
impl FromArma for MemoryReport {
fn from_arma(s: String) -> Result<Self, FromArmaError> {
let (total, free, avail) = <(u64, u64, u64)>::from_arma(s)?;
Ok(Self { total, free, avail })
}
}
impl IntoArma for MemoryReport {
fn to_arma(&self) -> Value {
Value::Array(
vec![self.total, self.free, self.avail]
.into_iter()
.map(|v| v.to_string().to_arma())
.collect(),
)
}
}
```
### Derive
Alternatively you can derive these traits. Note that the derive and manual implementation examples slightly differ, as when deriving map like structs its represented as an hashmap rather than an array. For more information on data representation and attributes see: [FromArma](https://docs.rs/arma-rs/latest/arma_rs/derive.FromArma.html) and [IntoArma](https://docs.rs/arma-rs/latest/arma_rs/derive.IntoArma.html).
```rust
use arma_rs::{FromArma, IntoArma};
#[derive(FromArma, IntoArma)]
struct MemoryReport {
#[arma(to_string)]
total: u64,
#[arma(to_string)]
free: u64,
#[arma(to_string)]
avail: u64,
}
```
Deriving is currently only supported for structs, this might change in the future.
## Error Codes
By default arma-rs will only allow commands via `RvExtensionArgs`. Using `callExtension` with only a function name will return an empty string.
```sqf
"my_extension" callExtension "hello:english" // returns ""
"my_extension" callExtension ["hello:english", []] // returns ["Hello", 0, 0]
```
This behaviour can be changed by calling `.allow_no_args()` when building the extension. It is recommended not to use this, and to implement error handling instead.
| Code | Description |
|------|---------------------------------------------------|
| 0 | Success |
| 1 | Command not found |
| 2x | Invalid argument count, x is received count |
| 3x | Invalid argument type, x is argument position |
| 4 | Attempted to write a value larger than the buffer |
| 9 | Application error, from using a Result |
### Error Examples
```rust
use arma_rs::Context;
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn overflow(ctx: Context) -> String {
"X".repeat(ctx.buffer_len() + 1)
}
pub fn should_error(error: bool) -> Result<String, String> {
if error {
Err(String::from("told to error"))
} else {
Ok(String::from("told to succeed"))
}
}
```
```sqf
"my_extension" callExtension ["add", [1, 2]]; // Returns ["3", 0, 0]
"my_extension" callExtension ["sub", [1, 2]]; // Returns ["", 1, 0]
"my_extension" callExtension ["add", [1, 2, 3]]; // Returns ["", 23, 0], didn't expect 3 elements
"my_extension" callExtension ["add", [1, "two"]]; // Returns ["", 31, 0], unable to parse the second argument
"my_extension" callExtension ["overflow", []]; // Returns ["", 4, 0], the return size was larger than the buffer
"my_extension" callExtension ["should_error", [true]]; // Returns ["told to error", 9, 0]
"my_extension" callExtension ["should_error", [false]]; // Returns ["told to succeed", 0, 0]
```
## Testing
Tests can be created utilizing the `extension.call()` method.
```rust,ignore
mod tests {
#[test]
fn hello() {
let extension = init().testing();
let (output, _) = extension.call("hello:english", None);
assert_eq!(output, "hello");
}
#[test]
fn welcome() {
let extension = init().testing();
let (output, _) =
extension.call("welcome:english", Some(vec!["John".to_string()]));
assert_eq!(output, "Welcome John");
}
#[test]
fn sleep_1sec() {
let extension = Extension::build()
.group("timer", super::group())
.finish()
.testing();
let (_, code) = extension.call(
"timer:sleep",
Some(vec!["1".to_string(), "test".to_string()]),
);
assert_eq!(code, 0);
let result = extension.callback_handler(
|name, func, data| {
assert_eq!(name, "timer:sleep");
assert_eq!(func, "done");
if let Some(Value::String(s)) = data {
Result::Ok(s)
} else {
Result::Err("Data was not a string".to_string())
}
},
Duration::from_secs(2),
);
assert_eq!(Result::Ok("test".to_string()), result);
}
}
```
## Unit Loadout Array
arma-rs includes a [loadout module](https://docs.rs/arma-rs/latest/arma_rs/loadout/index.html) to assist with the handling of [Arma's Unit Loadout Array](https://community.bistudio.com/wiki/Unit_Loadout_Array).
```rust
use arma_rs::{FromArma, loadout::{Loadout, InventoryItem, Weapon, Magazine}};
let l = r#"[[],[],[],["U_Marshal",[]],[],[],"H_Cap_headphones","G_Aviator",[],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]]"#;
let mut loadout = Loadout::from_arma(l.to_string()).unwrap();
loadout.set_secondary({
let mut weapon = Weapon::new("launch_B_Titan_short_F".to_string());
weapon.set_primary_magazine(Magazine::new("Titan_AT".to_string(), 1));
weapon
});
loadout.set_primary({
let mut weapon = Weapon::new("arifle_MXC_F".to_string());
weapon.set_optic("optic_Holosight".to_string());
weapon
});
let uniform = loadout.uniform_mut();
uniform.set_class("U_B_CombatUniform_mcam".to_string());
let uniform_items = uniform.items_mut().unwrap();
uniform_items.push(InventoryItem::new_item("FirstAidKit".to_string(), 3));
uniform_items.push(InventoryItem::new_magazine("30Rnd_65x39_caseless_mag".to_string(), 5, 30));
```
## Common Rust Libraries
arma-rs supports some common Rust libraries.
You can enable their support by adding their name to the features of arma-rs.
```toml
arma-rs = { version = "1.8.0", features = ["chrono"] }
```
Please create an issue first if you would like to add support for a new library.
### chrono
[`crates.io`](https://crates.io/crates/chrono)
#### chrono - Convert to Arma
[`NaiveDateTime`](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html) and [`DateTime<TimeZone>`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) will be converted to [Arma's date array](https://community.bistudio.com/wiki/systemTimeUTC).
The timezone will always be converted to UTC.
#### chrono - Convert From Arma
[Arma's date array](https://community.bistudio.com/wiki/systemTimeUTC) can be converted to [`NaiveDateTime`](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html).
### uuid
[`crates.io`](https://crates.io/crates/uuid)
#### uuid - Convert To Arma
[`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) will be converted to a string.
### serde_json
[`crates.io`](https://crates.io/crates/serde_json)
#### serde_json - Convert To Arma
Any variant of [`serde_json::Value`](https://docs.serde.rs/serde_json/enum.Value.html) will be converted to the appropriate Arma type.
## Building for x86 (32 Bit)
```sh
rustup toolchain install stable-i686-pc-windows-msvc
cargo +stable-i686-pc-windows-msvc build
```
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

16
vendor/arma-rs/build.rs vendored Normal file
View File

@@ -0,0 +1,16 @@
use std::path::Path;
fn main() {
let mut root = Path::new("../../README.md");
if !root.exists() {
root = Path::new("../README.md");
}
if !root.exists() {
root = Path::new("README.md");
}
std::fs::copy(
root,
Path::new(&format!("{}/README.md", std::env::var("OUT_DIR").unwrap())),
)
.unwrap();
}

40
vendor/arma-rs/examples/hello_world.rs vendored Normal file
View File

@@ -0,0 +1,40 @@
use arma_rs::{arma, Extension};
#[arma]
fn init() -> Extension {
Extension::build()
.version("1.0.0".to_string())
.command("hello", hello)
.command("welcome", welcome)
.finish()
}
pub fn hello() -> &'static str {
"Hello"
}
pub fn welcome(name: String) -> String {
format!("Welcome {name}")
}
#[cfg(test)]
mod tests {
use super::init;
#[test]
fn hello() {
let extension = init().testing();
let (result, _) = extension.call("hello", None);
assert_eq!(result, "Hello");
}
#[test]
fn welcome() {
let extension = init().testing();
let (result, _) = extension.call("welcome", Some(vec!["John".to_string()]));
assert_eq!(result, "Welcome John");
}
}
// Only required for cargo, don't include in your library
fn main() {}

364
vendor/arma-rs/src/call_context/call.rs vendored Normal file
View File

@@ -0,0 +1,364 @@
use std::path::Path;
use super::stack::ArmaContextStackTrace;
#[repr(C)]
struct RawArmaCallContext {
pub steam_id: u64,
pub source: *const libc::c_char,
pub mission: *const libc::c_char,
pub server: *const libc::c_char,
pub remote_exec_owner: i16,
pub call_stack: Option<*const super::stack::RawContextStackTrace>,
}
impl RawArmaCallContext {
fn from_arma(args: *mut *mut i8, count: libc::c_int) -> Self {
let steam_id = unsafe { *args.offset(0) as u64 };
let source = unsafe { *args.offset(1) as *const libc::c_char };
let mission = unsafe { *args.offset(2) as *const libc::c_char };
let server = unsafe { *args.offset(3) as *const libc::c_char };
let remote_exec_owner = unsafe { *args.offset(4) as i16 };
let call_stack = if count > 5 {
let stack = unsafe { *args.offset(5) as *const super::stack::RawContextStackTrace };
Some(stack)
} else {
None
};
Self {
steam_id,
source,
mission,
server,
remote_exec_owner,
call_stack,
}
}
}
pub trait StackRequest {}
pub struct WithStackTrace;
impl StackRequest for WithStackTrace {}
pub struct WithoutStackTrace;
impl StackRequest for WithoutStackTrace {}
/// Context of the callExtension, provided by Arma.
pub type CallContext = ArmaCallContext<WithoutStackTrace>;
/// Context of the callExtension, provided by Arma, with a stack trace.
pub type CallContextStackTrace = ArmaCallContext<WithStackTrace>;
#[derive(Clone, Default)]
/// Context of the Arma call.
pub struct ArmaCallContext<T: StackRequest> {
pub(super) caller: Caller,
pub(super) source: Source,
pub(super) mission: Mission,
pub(super) server: Server,
pub(super) remote_exec_owner: i16,
_stack_marker: std::marker::PhantomData<T>,
stack: Option<ArmaContextStackTrace>,
}
impl<T: StackRequest> ArmaCallContext<T> {
pub(crate) const fn new(
caller: Caller,
source: Source,
mission: Mission,
server: Server,
remote_exec_owner: i16,
) -> Self {
Self {
caller,
source,
mission,
server,
remote_exec_owner,
_stack_marker: std::marker::PhantomData,
stack: None,
}
}
/// Create a new ArmaCallContext from pointers provided by Arma.
pub fn from_arma(args: *mut *mut i8, count: libc::c_int) -> Self {
let raw = RawArmaCallContext::from_arma(args, count);
Self {
caller: Caller::Steam(raw.steam_id),
source: Source::from(unsafe { std::ffi::CStr::from_ptr(raw.source).to_str().unwrap() }),
mission: Mission::from(unsafe {
std::ffi::CStr::from_ptr(raw.mission).to_str().unwrap()
}),
server: Server::from(unsafe { std::ffi::CStr::from_ptr(raw.server).to_str().unwrap() }),
remote_exec_owner: raw.remote_exec_owner,
_stack_marker: std::marker::PhantomData,
stack: raw.call_stack.map(ArmaContextStackTrace::from),
}
}
#[must_use]
/// Player that called the extension. Can be [`Caller::Unknown`] when the player's steamID64 is unavailable
/// # Note
/// Unlike <https://community.bistudio.com/wiki/getPlayerUID> [`Caller::Steam`] isn't limited to multiplayer.
pub fn caller(&self) -> &Caller {
&self.caller
}
#[must_use]
/// Source from where the extension was called.
pub fn source(&self) -> &Source {
&self.source
}
#[must_use]
/// Current mission's name.
/// # Note
/// Can result in [`Mission::None`] in missions made prior to Arma v2.02.
pub fn mission(&self) -> &Mission {
&self.mission
}
#[must_use]
/// Current server's name
pub fn server(&self) -> &Server {
&self.server
}
#[must_use]
/// Remote execution owner.
pub fn remote_exec_owner(&self) -> i16 {
self.remote_exec_owner
}
}
impl ArmaCallContext<WithStackTrace> {
#[must_use]
/// Call stack of the extension call.
pub fn stack_trace(&self) -> &ArmaContextStackTrace {
// By the time this gets to consumer code, to_without_stack would've been called if the stack was not requested
self.stack.as_ref().expect("Stack is missing")
}
/// Convert the context to one without a stack trace.
pub(crate) fn into_without_stack(self) -> ArmaCallContext<WithoutStackTrace> {
ArmaCallContext::new(
self.caller,
self.source,
self.mission,
self.server,
self.remote_exec_owner,
)
}
}
/// Identification of the player calling your extension.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Caller {
/// The player's steamID64.
Steam(u64),
#[default]
/// Unable to determine.
Unknown,
}
impl Caller {
/// Convert the caller to a string.
pub fn as_str(&self) -> String {
match self {
Self::Steam(id) => id.to_string(),
Self::Unknown => "0".to_string(),
}
}
/// Convert the caller to a u64.
pub fn as_u64(&self) -> u64 {
match self {
Self::Steam(id) => *id,
Self::Unknown => 0,
}
}
}
impl From<&str> for Caller {
fn from(s: &str) -> Self {
if s.is_empty() || s == "0" {
Self::Unknown
} else {
s.parse::<u64>().map_or(Self::Unknown, Self::Steam)
}
}
}
impl From<u64> for Caller {
fn from(id: u64) -> Self {
if id == 0 {
Self::Unknown
} else {
Self::Steam(id)
}
}
}
/// Source of the extension call.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Source {
/// Absolute path of the file on the players system.
/// For example on windows: `C:\Users\user\Documents\Arma 3\missions\test.VR\fn_armaContext.sqf`.
File(String),
/// Path inside of a pbo.
/// For example: `z\test\addons\main\fn_armaContext.sqf`.
Pbo(String),
#[default]
/// Debug console or an other form of on the fly execution, such as mission triggers.
Console,
}
impl Source {
/// Convert the source to a string.
pub fn as_str(&self) -> &str {
match self {
Self::File(s) | Self::Pbo(s) => s,
Self::Console => "",
}
}
}
impl From<&str> for Source {
fn from(s: &str) -> Self {
if s.is_empty() {
Self::Console
} else if Path::new(s).is_absolute() {
Self::File(s.to_string())
} else {
Self::Pbo(s.to_string())
}
}
}
impl From<*const libc::c_char> for Source {
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn from(s: *const libc::c_char) -> Self {
Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
}
}
/// Current mission.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Mission {
/// Mission name.
Mission(String),
#[default]
/// Not in a mission.
None,
}
impl Mission {
/// Convert the mission to a string.
pub fn as_str(&self) -> &str {
match self {
Self::Mission(s) => s,
Self::None => "",
}
}
}
impl From<&str> for Mission {
fn from(s: &str) -> Self {
if s.is_empty() {
Self::None
} else {
Self::Mission(s.to_string())
}
}
}
impl From<*const libc::c_char> for Mission {
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn from(s: *const libc::c_char) -> Self {
Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
}
}
/// Current server.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Server {
/// Server name
Multiplayer(String),
#[default]
/// Singleplayer or no mission
Singleplayer,
}
impl Server {
/// Convert the server to a string.
pub fn as_str(&self) -> &str {
match self {
Self::Multiplayer(s) => s,
Self::Singleplayer => "",
}
}
}
impl From<&str> for Server {
fn from(s: &str) -> Self {
if s.is_empty() {
Self::Singleplayer
} else {
Self::Multiplayer(s.to_string())
}
}
}
impl From<*const libc::c_char> for Server {
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn from(s: *const libc::c_char) -> Self {
Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caller_empty() {
assert_eq!(Caller::from(""), Caller::Unknown);
}
#[test]
fn caller_zero() {
assert_eq!(Caller::from("0"), Caller::Unknown);
}
#[test]
fn source_empty() {
assert_eq!(Source::from(""), Source::Console);
}
#[test]
fn source_pbo() {
let path = "x\\ctx\\addons\\main\\fn_armaContext.sqf";
assert_eq!(Source::from(path), Source::Pbo(path.to_string()));
}
#[test]
fn source_file() {
let path = env!("CARGO_MANIFEST_DIR");
assert_eq!(Source::from(path), Source::File(path.to_string()));
}
#[test]
fn mission_empty() {
assert_eq!(Mission::from(""), Mission::None);
}
#[test]
fn server_empty() {
assert_eq!(Server::from(""), Server::Singleplayer);
}
}

View File

@@ -0,0 +1,40 @@
use std::{cell::RefCell, rc::Rc};
use crate::ContextRequest;
use super::CallContextStackTrace;
/// Manages requesting and replacing the ArmaCallContext
pub struct ArmaContextManager {
pub(crate) request: RefCell<ContextRequest>,
state: Rc<RefCell<Option<CallContextStackTrace>>>,
}
impl ArmaContextManager {
/// Create a new ArmaContextManager
pub fn new(request: ContextRequest) -> Self {
Self {
request: RefCell::new(request),
state: Rc::new(RefCell::new(None)),
}
}
/// Request a new ArmaCallContext from Arma
pub fn request(&self) -> CallContextStackTrace {
// When the request is called, Arma will send the request to the extension
// The extension will set the state to the request it just received
unsafe {
(self.request.borrow())();
}
// When the request function returns, the state has been set by Arma
// It can now be taken and sent to the Context
self.state
.replace(None)
.expect("Arma should've set the state")
}
/// Replace the current ArmaCallContext with a new one
pub fn replace(&self, value: Option<CallContextStackTrace>) {
*self.state.borrow_mut() = value;
}
}

View File

@@ -0,0 +1,6 @@
mod call;
mod manager;
mod stack;
pub use call::*;
pub use manager::ArmaContextManager;

View File

@@ -0,0 +1,72 @@
use std::ffi::CStr;
#[repr(C)]
pub struct RawStackTraceLine {
// Line number in file (before preprocessing if preprocessed with line numbers)
pub line_number: u32,
// File offset in bytes from the start of the file (after preprocessing)
pub file_offset: u32,
// Filepath to the source file
pub source_file: *const libc::c_char,
// scopeName set on that level
pub scope_name: *const libc::c_char,
// Complete fileContent of the sourceFile (after preprocessing, can be combined with fileOffset to find exact location)
pub file_content: *const libc::c_char,
}
#[repr(C)]
pub struct RawContextStackTrace {
pub lines: *mut RawStackTraceLine,
pub line_count: u32,
}
impl RawContextStackTrace {
pub fn to_lines(&self) -> Option<&[RawStackTraceLine]> {
unsafe {
self.lines
.as_ref()
.map(|lines_ptr| std::slice::from_raw_parts(lines_ptr, self.line_count as usize))
}
}
}
#[derive(Debug, Clone)]
pub struct ArmaContextStackTrace {
pub lines: Vec<ArmaStackTraceLine>,
}
#[derive(Debug, Clone)]
pub struct ArmaStackTraceLine {
pub line_number: u32,
pub file_offset: u32,
pub source_file: String,
pub scope_name: String,
pub file_content: String,
}
impl From<*const RawContextStackTrace> for ArmaContextStackTrace {
fn from(raw: *const RawContextStackTrace) -> Self {
unsafe {
let raw = raw.as_ref().unwrap();
let lines = raw
.to_lines()
.unwrap()
.iter()
.map(|line| ArmaStackTraceLine {
line_number: line.line_number,
file_offset: line.file_offset,
source_file: CStr::from_ptr(line.source_file)
.to_string_lossy()
.into_owned(),
scope_name: CStr::from_ptr(line.scope_name)
.to_string_lossy()
.into_owned(),
file_content: CStr::from_ptr(line.file_content)
.to_string_lossy()
.into_owned(),
})
.collect();
Self { lines }
}
}
}

334
vendor/arma-rs/src/command.rs vendored Normal file
View File

@@ -0,0 +1,334 @@
use crate::call_context::{ArmaContextManager, CallContext, CallContextStackTrace};
use crate::ext_result::IntoExtResult;
use crate::flags::FeatureFlags;
use crate::value::{FromArma, Value};
use crate::Context;
type HandlerFunc = Box<
dyn Fn(
Context,
&ArmaContextManager,
*mut libc::c_char,
libc::size_t,
Option<*mut *mut i8>,
Option<libc::c_int>,
) -> libc::c_int,
>;
#[doc(hidden)]
/// A wrapper for `HandlerFunc`
pub struct Handler {
/// The function to call
pub handler: HandlerFunc,
}
#[doc(hidden)]
/// Create a new handler from a Factory
pub fn fn_handler<C, I, R>(command: C) -> Handler
where
C: Factory<I, R> + 'static,
{
Handler {
handler: Box::new(
move |context: Context,
acm: &ArmaContextManager,
output: *mut libc::c_char,
size: libc::size_t,
args: Option<*mut *mut i8>,
count: Option<libc::c_int>|
-> libc::c_int {
unsafe { command.call(context, acm, output, size, args, count) }
},
),
}
}
#[doc(hidden)]
/// Execute a command
pub trait Executor: 'static {
/// # Safety
/// This function is unsafe because it interacts with the C API.
unsafe fn call(
&self,
context: Context,
acm: &ArmaContextManager,
output: *mut libc::c_char,
size: libc::size_t,
args: Option<*mut *mut i8>,
count: Option<libc::c_int>,
);
}
#[doc(hidden)]
/// A factory for creating a command handler.
/// Creates a handler from any function that optionally takes a context and up to 12 arguments.
/// The arguments must implement `FromArma`
/// The return value must implement `IntoExtResult`
pub trait Factory<A, R> {
/// # Safety
/// This function is unsafe because it interacts with the C API.
unsafe fn call(
&self,
context: Context,
acm: &ArmaContextManager,
output: *mut libc::c_char,
size: libc::size_t,
args: Option<*mut *mut i8>,
count: Option<libc::c_int>,
) -> libc::c_int;
}
macro_rules! execute {
($s:ident, $c:expr, $count:expr, $output:expr, $size:expr, $args:expr, ($( $vars:ident )*), ($( $param:ident, )*)) => {{
let count = $count.unwrap_or_else(|| 0);
if count != $c {
return format!("2{}", count).parse::<libc::c_int>().unwrap();
}
if $c == 0 {
handle_output_and_return(
($s)($( $vars, )* $($param::from_arma("".to_string()).unwrap(),)*),
$output,
$size
)
} else {
#[allow(unused_variables, unused_mut)]
let mut argv: Vec<String> = {
let argv: &[*mut libc::c_char; $c] = &*($args.unwrap() as *const [*mut i8; $c]);
let mut argv = argv
.to_vec()
.into_iter()
.map(|s| {
std::ffi::CStr::from_ptr(s).to_string_lossy().to_string()
})
.collect::<Vec<String>>();
argv.reverse();
argv
};
#[allow(unused_variables, unused_mut)] // Caused by the 0 loop
let mut c = 0;
#[allow(unused_assignments, clippy::mixed_read_write_in_expression)]
handle_output_and_return(
{
($s)($( $vars, )* $(
if let Ok(val) = $param::from_arma(argv.pop().unwrap()) {
c += 1;
val
} else {
return format!("3{}", c).parse::<libc::c_int>().unwrap()
},
)*)
},
$output,
$size
)
}
}};
}
macro_rules! factory_tuple ({ $c: expr, $($param:ident)* } => {
impl<$($param,)* ER> Executor for dyn Factory<($($param,)*), ER>
where
ER: 'static,
$($param: FromArma + 'static,)*
{
unsafe fn call(
&self,
context: Context,
acm: &ArmaContextManager,
output: *mut libc::c_char,
size: libc::size_t,
args: Option<*mut *mut i8>,
count: Option<libc::c_int>,
) {
self.call(context, acm, output, size, args, count);
}
}
// No context
impl<Func, $($param,)* ER> Factory<($($param,)*), ER> for Func
where
ER: IntoExtResult + 'static,
Func: Fn($($param),*) -> ER,
$($param: FromArma,)*
{
#[allow(non_snake_case)]
unsafe fn call(&self, _: Context, _: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
let count = count.unwrap_or_else(|| 0);
if count != $c {
return format!("2{}", count).parse::<libc::c_int>().unwrap();
}
if $c == 0 {
handle_output_and_return(
(self)($($param::from_arma("".to_string()).unwrap(),)*),
output,
size
)
} else {
#[allow(unused_variables, unused_mut)]
let mut argv: Vec<String> = {
let argv: &[*mut libc::c_char; $c] = &*(args.unwrap() as *const [*mut i8; $c]);
let mut argv = argv
.to_vec()
.into_iter()
.map(|s| {
std::ffi::CStr::from_ptr(s).to_string_lossy().to_string()
})
.collect::<Vec<String>>();
argv.reverse();
argv
};
#[allow(unused_variables, unused_mut)] // Caused by the 0 loop
let mut c = 0;
#[allow(unused_assignments, clippy::mixed_read_write_in_expression)]
handle_output_and_return(
{
(self)($(
if let Ok(val) = $param::from_arma(argv.pop().unwrap()) {
c += 1;
val
} else {
return format!("3{}", c).parse::<libc::c_int>().unwrap()
},
)*)
},
output,
size
)
}
}
}
// Context
impl<Func, $($param,)* ER> Factory<(Context, $($param,)*), ER> for Func
where
ER: IntoExtResult + 'static,
Func: Fn(Context, $($param),*) -> ER,
$($param: FromArma,)*
{
#[allow(non_snake_case)]
unsafe fn call(&self, context: Context, _: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
execute!(self, $c, count, output, size, args, (context), ($($param,)*))
}
}
// Call Context
impl<Func, $($param,)* ER> Factory<(CallContext, $($param,)*), ER> for Func
where
ER: IntoExtResult + 'static,
Func: Fn(CallContext, $($param),*) -> ER,
$($param: FromArma,)*
{
#[allow(non_snake_case)]
unsafe fn call(&self, _: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(false).as_bits();
let call_context = acm.request().into_without_stack();
execute!(self, $c, count, output, size, args, (call_context), ($($param,)*))
}
}
// Call Context with Stack Trace
impl<Func, $($param,)* ER> Factory<(CallContextStackTrace, $($param,)*), ER> for Func
where
ER: IntoExtResult + 'static,
Func: Fn(CallContextStackTrace, $($param),*) -> ER,
$($param: FromArma,)*
{
#[allow(non_snake_case)]
unsafe fn call(&self, _: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(true).as_bits();
let call_context = acm.request();
execute!(self, $c, count, output, size, args, (call_context), ($($param,)*))
}
}
// Context & Call Context
impl<Func, $($param,)* ER> Factory<(Context, CallContext, $($param,)*), ER> for Func
where
ER: IntoExtResult + 'static,
Func: Fn(Context, CallContext, $($param),*) -> ER,
$($param: FromArma,)*
{
#[allow(non_snake_case)]
unsafe fn call(&self, context: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(false).as_bits();
let call_context = acm.request().into_without_stack();
execute!(self, $c, count, output, size, args, (context call_context), ($($param,)*))
}
}
// Context & Call Context with Stack Trace
impl<Func, $($param,)* ER> Factory<(Context, CallContextStackTrace, $($param,)*), ER> for Func
where
ER: IntoExtResult + 'static,
Func: Fn(Context, CallContextStackTrace, $($param),*) -> ER,
$($param: FromArma,)*
{
#[allow(non_snake_case)]
unsafe fn call(&self, context: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(true).as_bits();
let call_context = acm.request();
execute!(self, $c, count, output, size, args, (context call_context), ($($param,)*))
}
}
});
unsafe fn handle_output_and_return<R>(
ret: R,
output: *mut libc::c_char,
size: libc::size_t,
) -> libc::c_int
where
R: IntoExtResult + 'static,
{
let ret = ret.to_ext_result();
let ok = ret.is_ok();
if crate::write_cstr(
{
let value = match ret {
Ok(x) | Err(x) => x,
};
match value {
Value::String(s) => s,
v => v.to_string(),
}
},
output,
size,
)
.is_none()
{
4
} else if ok {
0
} else {
9
}
}
factory_tuple! { 0, }
factory_tuple! { 1, A }
factory_tuple! { 2, A B }
factory_tuple! { 3, A B C }
factory_tuple! { 4, A B C D }
factory_tuple! { 5, A B C D E }
factory_tuple! { 6, A B C D E F }
factory_tuple! { 7, A B C D E F G }
factory_tuple! { 8, A B C D E F G H }
factory_tuple! { 9, A B C D E F G H I }
factory_tuple! { 10, A B C D E F G H I J }
factory_tuple! { 11, A B C D E F G H I J K }
factory_tuple! { 12, A B C D E F G H I J K L }
factory_tuple! { 13, A B C D E F G H I J K L M }
factory_tuple! { 14, A B C D E F G H I J K L M N }
factory_tuple! { 15, A B C D E F G H I J K L M N O }
factory_tuple! { 16, A B C D E F G H I J K L M N O P }
factory_tuple! { 17, A B C D E F G H I J K L M N O P Q }
factory_tuple! { 18, A B C D E F G H I J K L M N O P Q R }
factory_tuple! { 19, A B C D E F G H I J K L M N O P Q R S }
factory_tuple! { 20, A B C D E F G H I J K L M N O P Q R S T }
factory_tuple! { 21, A B C D E F G H I J K L M N O P Q R S T U }
factory_tuple! { 22, A B C D E F G H I J K L M N O P Q R S T U V }
factory_tuple! { 23, A B C D E F G H I J K L M N O P Q R S T U V W }
factory_tuple! { 24, A B C D E F G H I J K L M N O P Q R S T U V W X }
factory_tuple! { 25, A B C D E F G H I J K L M N O P Q R S T U V W X Y }
factory_tuple! { 26, A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }

37
vendor/arma-rs/src/context/global.rs vendored Normal file
View File

@@ -0,0 +1,37 @@
use std::sync::Arc;
use crate::{ContextState, State};
/// Contains information about the extension
pub struct GlobalContext {
version: String,
state: Arc<State>,
}
impl GlobalContext {
pub(crate) fn new(version: String, state: Arc<State>) -> Self {
Self { version, state }
}
#[must_use]
/// Version of the Arma extension
pub fn version(&self) -> &str {
&self.version
}
}
impl ContextState for GlobalContext {
fn get<T>(&self) -> Option<&T>
where
T: Send + Sync + 'static,
{
self.state.try_get()
}
fn set<T>(&self, value: T) -> bool
where
T: Send + Sync + 'static,
{
self.state.set(value)
}
}

30
vendor/arma-rs/src/context/group.rs vendored Normal file
View File

@@ -0,0 +1,30 @@
use std::sync::Arc;
use crate::{ContextState, State};
/// Contains information about the current group
pub struct GroupContext {
state: Arc<State>,
}
impl GroupContext {
pub(crate) fn new(state: Arc<State>) -> Self {
Self { state }
}
}
impl ContextState for GroupContext {
fn get<T>(&self) -> Option<&T>
where
T: Send + Sync + 'static,
{
self.state.try_get()
}
fn set<T>(&self, value: T) -> bool
where
T: Send + Sync + 'static,
{
self.state.set(value)
}
}

171
vendor/arma-rs/src/context/mod.rs vendored Normal file
View File

@@ -0,0 +1,171 @@
//! Contextual execution information.
use crossbeam_channel::Sender;
use crate::{CallbackMessage, IntoArma, Value};
mod global;
mod group;
mod state;
pub use self::state::ContextState;
pub use global::GlobalContext;
pub use group::GroupContext;
/// Contains information about the current execution context
pub struct Context {
callback_tx: Sender<CallbackMessage>,
global: GlobalContext,
group: GroupContext,
buffer_size: usize,
}
impl Context {
pub(crate) fn new(
callback_tx: Sender<CallbackMessage>,
global: GlobalContext,
group: GroupContext,
) -> Self {
Self {
callback_tx,
global,
group,
buffer_size: 0,
}
}
pub(crate) fn with_group(mut self, ctx: GroupContext) -> Self {
self.group = ctx;
self
}
pub(crate) const fn with_buffer_size(mut self, buffer_size: usize) -> Self {
self.buffer_size = buffer_size;
self
}
#[must_use]
/// Global context
pub const fn global(&self) -> &GlobalContext {
&self.global
}
/// Group context, is equal to `GlobalContext` if the call is from the global scope.
pub const fn group(&self) -> &GroupContext {
&self.group
}
#[must_use]
/// Returns the length in bytes of the output buffer.
/// This is the maximum size of the data that can be returned by the extension.
pub const fn buffer_len(&self) -> usize {
if self.buffer_size == 0 {
0
} else {
self.buffer_size - 1
}
}
fn callback(&self, name: &str, func: &str, data: Option<Value>) -> Result<(), CallbackError> {
self.callback_tx
.send(CallbackMessage::Call(
name.to_string(),
func.to_string(),
data,
))
.map_err(|_| CallbackError::ChannelClosed)
}
/// Sends a callback with data into Arma
/// <https://community.bistudio.com/wiki/Arma_3:_Mission_Event_Handlers#ExtensionCallback>
pub fn callback_data<V>(&self, name: &str, func: &str, data: V) -> Result<(), CallbackError>
where
V: IntoArma,
{
self.callback(name, func, Some(data.to_arma()))
}
/// Sends a callback without data into Arma
/// <https://community.bistudio.com/wiki/Arma_3:_Mission_Event_Handlers#ExtensionCallback>
pub fn callback_null(&self, name: &str, func: &str) -> Result<(), CallbackError> {
self.callback(name, func, None)
}
}
/// Error that can occur when sending a callback
#[derive(Debug)]
pub enum CallbackError {
/// The callback channel has been closed
ChannelClosed,
}
impl std::fmt::Display for CallbackError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ChannelClosed => write!(f, "Callback channel closed"),
}
}
}
impl IntoArma for CallbackError {
fn to_arma(&self) -> Value {
Value::String(self.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::State;
use crossbeam_channel::{bounded, Sender};
use std::sync::Arc;
fn context(tx: Sender<CallbackMessage>) -> Context {
Context::new(
tx,
GlobalContext::new(String::new(), Arc::new(State::default())),
GroupContext::new(Arc::new(State::default())),
)
}
#[test]
fn context_buffer_len_zero() {
let (tx, _) = bounded(0);
assert_eq!(context(tx).buffer_len(), 0);
}
#[test]
fn context_buffer_len() {
let (tx, _) = bounded(0);
assert_eq!(context(tx).with_buffer_size(100).buffer_len(), 99);
}
#[test]
fn context_callback_block() {
let (tx, rx) = bounded(0);
let callback_tx = tx.clone();
std::thread::spawn(|| {
context(callback_tx).callback_null("", "").unwrap();
});
let callback_tx = tx;
std::thread::spawn(|| {
context(callback_tx).callback_data("", "", "").unwrap();
});
std::thread::sleep(std::time::Duration::from_millis(50));
assert_eq!(rx.iter().count(), 2);
}
#[test]
fn context_callback_closed() {
let (tx, _) = bounded(0);
assert!(matches!(
context(tx.clone()).callback_null("", ""),
Err(CallbackError::ChannelClosed)
));
assert!(matches!(
context(tx).callback_data("", "", ""),
Err(CallbackError::ChannelClosed)
));
}
}

12
vendor/arma-rs/src/context/state.rs vendored Normal file
View File

@@ -0,0 +1,12 @@
/// A trait for accessing state values
pub trait ContextState {
/// Get a reference to a state value
fn get<T>(&self) -> Option<&T>
where
T: Send + Sync + 'static;
/// Set a state value
fn set<T>(&self, value: T) -> bool
where
T: Send + Sync + 'static;
}

98
vendor/arma-rs/src/ext_result.rs vendored Normal file
View File

@@ -0,0 +1,98 @@
use crate::value::{IntoArma, Value};
/// Convert a type to a successful or failed extension result
pub trait IntoExtResult {
/// Convert a type to a successful or failed extension result
fn to_ext_result(&self) -> Result<Value, Value>;
}
impl IntoExtResult for Value {
fn to_ext_result(&self) -> Result<Value, Value> {
Ok(self.to_owned())
}
}
impl<T> IntoExtResult for T
where
T: IntoArma,
{
fn to_ext_result(&self) -> Result<Value, Value> {
self.to_arma().to_ext_result()
}
}
impl IntoExtResult for Result<Value, Value> {
fn to_ext_result(&self) -> Result<Value, Value> {
self.to_owned()
}
}
impl<T, E> IntoExtResult for Result<T, E>
where
T: IntoArma,
E: IntoArma,
{
fn to_ext_result(&self) -> Result<Value, Value> {
match self {
Ok(v) => Ok(v.to_arma()),
Err(e) => Err(e.to_arma()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn value() {
assert_eq!(
Ok(Value::Boolean(true)),
Value::Boolean(true).to_ext_result()
);
}
#[test]
fn option_none() {
assert_eq!(Ok(Value::Null), None::<&str>.to_ext_result());
}
#[test]
fn option_some() {
assert_eq!(
Ok(Value::String("Hello".into())),
Some("Hello".to_string()).to_ext_result()
);
}
#[test]
fn result_ok() {
assert_eq!(
Ok(Value::Number(42.0)),
Ok(Value::Number(42.0)).to_ext_result()
);
}
#[test]
fn result_err() {
assert_eq!(
Err(Value::String("Hello".into())),
Err(Value::String("Hello".into())).to_ext_result()
);
}
#[test]
fn result_unit_ok() {
assert_eq!(Ok(Value::Null), Ok::<(), String>(()).to_ext_result());
}
#[test]
fn result_unit_err() {
assert_eq!(Err(Value::Null), Err::<String, ()>(()).to_ext_result());
}
#[test]
fn result_unit_both() {
assert_eq!(Ok(Value::Null), Ok::<(), ()>(()).to_ext_result());
}
}

73
vendor/arma-rs/src/flags.rs vendored Normal file
View File

@@ -0,0 +1,73 @@
//! Feature flags for RV Extensions
//!
//! <https://community.bistudio.com/wiki/Extensions#Feature_Flags>
/// RVExtensionContext takes const void** as argument, instead of the default const char**, and arguments will be passed in their custom types
pub const RV_CONTEXT_ARGUMENTS_VOID_PTR: u64 = 1 << 0;
/// RVExtensionContext will retrieve a full Stacktrace
pub const RV_CONTEXT_STACK_TRACE: u64 = 1 << 1;
/// RVExtensionContext will not be called automatically. It must be manually requested via RVExtensionRequestContext (This improves performance when context is not needed).
pub const RV_CONTEXT_NO_DEFAULT_CALL: u64 = 1 << 2;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
/// Feature flags for RV Extensions
pub struct FeatureFlags {
context_stack_trace: bool,
}
impl FeatureFlags {
/// Set the context_stack_trace flag
pub fn set_context_stack_trace(&mut self, value: bool) {
self.context_stack_trace = value;
}
pub fn with_context_stack_trace(mut self, value: bool) -> Self {
self.set_context_stack_trace(value);
self
}
/// Get the context_stack_trace flag
pub fn context_stack_trace(&self) -> bool {
self.context_stack_trace
}
/// Create a new FeatureFlags from the given bits
pub fn from_bits(bits: u64) -> Self {
let mut flags = Self::default();
flags.set_context_stack_trace(bits & RV_CONTEXT_STACK_TRACE != 0);
flags
}
/// Get the bits of the FeatureFlags
pub fn as_bits(&self) -> u64 {
let mut bits = RV_CONTEXT_NO_DEFAULT_CALL | RV_CONTEXT_ARGUMENTS_VOID_PTR;
if self.context_stack_trace() {
bits |= RV_CONTEXT_STACK_TRACE;
}
bits
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
let flags = FeatureFlags::default();
assert_eq!(
flags.as_bits(),
RV_CONTEXT_NO_DEFAULT_CALL | RV_CONTEXT_ARGUMENTS_VOID_PTR
);
}
#[test]
fn just_stack_trace() {
let mut flags = FeatureFlags::default();
flags.set_context_stack_trace(true);
assert_eq!(
flags.as_bits(),
RV_CONTEXT_NO_DEFAULT_CALL | RV_CONTEXT_STACK_TRACE | RV_CONTEXT_ARGUMENTS_VOID_PTR
);
}
}

123
vendor/arma-rs/src/group.rs vendored Normal file
View File

@@ -0,0 +1,123 @@
use std::{collections::HashMap, sync::Arc};
use crate::{
command::{fn_handler, Factory, Handler},
context::{Context, GroupContext},
State,
};
#[derive(Default)]
/// A group of commands.
/// Called from Arma using `[group]:[command]`.
pub struct Group {
commands: HashMap<String, Box<Handler>>,
children: HashMap<String, Self>,
state: State,
}
impl Group {
#[must_use]
/// Creates a new group
pub fn new() -> Self {
Self {
commands: HashMap::new(),
children: HashMap::new(),
state: State::default(),
}
}
#[inline]
#[must_use]
/// Add a new state value to the group if it has not be added already
pub fn state<T>(self, state: T) -> Self
where
T: Send + Sync + 'static,
{
self.state.set(state);
self
}
#[inline]
#[must_use]
/// Freeze the group's state, preventing the state from changing, allowing for faster reads
pub fn freeze_state(mut self) -> Self {
self.state.freeze();
self
}
#[inline]
#[must_use]
/// Add a command to the group
pub fn command<S, F, I, R>(mut self, name: S, handler: F) -> Self
where
S: Into<String>,
F: Factory<I, R> + 'static,
{
self.commands
.insert(name.into(), Box::new(fn_handler(handler)));
self
}
#[inline]
#[must_use]
/// Add a group to the group
pub fn group<S>(mut self, name: S, child: Self) -> Self
where
S: Into<String>,
{
self.children.insert(name.into(), child);
self
}
}
pub(crate) struct InternalGroup {
commands: HashMap<String, Box<Handler>>,
children: HashMap<String, Self>,
pub(crate) state: Arc<State>,
}
impl InternalGroup {
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle(
&self,
context: Context,
acm: &crate::ArmaContextManager,
function: &str,
output: *mut libc::c_char,
size: libc::size_t,
args: Option<*mut *mut i8>,
count: Option<libc::c_int>,
) -> libc::c_int {
if let Some((group, function)) = function.split_once(':') {
self.children.get(group).map_or(1, |group| {
group.handle(context, acm, function, output, size, args, count)
})
} else if let Some(handler) = self.commands.get(function) {
(handler.handler)(
context.with_group(GroupContext::new(self.state.clone())),
acm,
output,
size,
args,
count,
)
} else {
1
}
}
}
impl From<Group> for InternalGroup {
fn from(group: Group) -> Self {
let children = group
.children
.into_iter()
.map(|(name, group)| (name, Self::from(group)))
.collect();
Self {
commands: group.commands,
children,
state: Arc::new(group.state),
}
}
}

520
vendor/arma-rs/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,520 @@
#![warn(missing_docs, nonstandard_style)]
#![doc = include_str!(concat!(env!("OUT_DIR"), "/README.md"))]
use std::rc::Rc;
pub use arma_rs_proc::{arma, FromArma, IntoArma};
#[cfg(feature = "extension")]
use crossbeam_channel::{unbounded, Receiver, Sender};
#[cfg(feature = "extension")]
pub use libc;
#[cfg(all(target_os = "windows", target_arch = "x86"))]
pub use link_args;
#[cfg(feature = "extension")]
#[macro_use]
extern crate log;
mod flags;
mod value;
pub use value::{loadout, DirectReturn, FromArma, FromArmaError, IntoArma, Value};
#[cfg(feature = "extension")]
mod call_context;
#[cfg(feature = "extension")]
use call_context::{ArmaCallContext, ArmaContextManager};
#[cfg(feature = "extension")]
pub use call_context::{CallContext, CallContextStackTrace, Caller, Mission, Server, Source};
#[cfg(feature = "extension")]
mod ext_result;
#[cfg(feature = "extension")]
pub use ext_result::IntoExtResult;
#[cfg(feature = "extension")]
mod command;
#[cfg(feature = "extension")]
pub use command::*;
#[cfg(feature = "extension")]
pub mod context;
#[cfg(feature = "extension")]
pub use context::*;
#[cfg(feature = "extension")]
mod group;
#[cfg(feature = "extension")]
pub use group::Group;
#[cfg(feature = "extension")]
pub mod testing;
#[cfg(feature = "extension")]
pub use testing::Result;
#[cfg(all(windows, feature = "extension"))]
#[doc(hidden)]
/// Used by generated code to call back into Arma
pub type Callback = extern "stdcall" fn(
*const libc::c_char,
*const libc::c_char,
*const libc::c_char,
) -> libc::c_int;
#[cfg(all(not(windows), feature = "extension"))]
#[doc(hidden)]
/// Used by generated code to call back into Arma
pub type Callback =
extern "C" fn(*const libc::c_char, *const libc::c_char, *const libc::c_char) -> libc::c_int;
/// Requests a call context from Arma
pub type ContextRequest = unsafe extern "C" fn();
#[cfg(feature = "extension")]
enum CallbackMessage {
Call(String, String, Option<Value>),
Terminate,
}
#[cfg(feature = "extension")]
/// State TypeMap that can hold at most one value per type key.
pub type State = state::TypeMap![Send + Sync];
#[cfg(windows)]
/// Allows a console to be allocated for the extension.
static CONSOLE_ALLOCATED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[no_mangle]
#[allow(non_upper_case_globals, reason = "This is a C API")]
/// Feature flags read on each callExtension call.
pub static mut RVExtensionFeatureFlags: u64 = flags::RV_CONTEXT_NO_DEFAULT_CALL;
/// Contains all the information about your extension
/// This is used by the generated code to interface with Arma
#[cfg(feature = "extension")]
pub struct Extension {
version: String,
group: group::InternalGroup,
allow_no_args: bool,
callback: Option<Callback>,
callback_channel: (Sender<CallbackMessage>, Receiver<CallbackMessage>),
callback_thread: Option<std::thread::JoinHandle<()>>,
context_manager: Rc<ArmaContextManager>,
pre218_clear_context_override: bool,
}
#[cfg(feature = "extension")]
impl Extension {
#[must_use]
/// Creates a new extension.
pub fn build() -> ExtensionBuilder {
ExtensionBuilder {
version: String::from("0.0.0"),
group: Group::new(),
allow_no_args: false,
}
}
}
#[cfg(feature = "extension")]
impl Extension {
#[must_use]
/// Returns the version of the extension.
pub fn version(&self) -> &str {
&self.version
}
#[must_use]
/// Returns if the extension can be called without any arguments.
/// Example:
/// ```sqf
/// "my_ext" callExtension "my_func"
/// ```
pub const fn allow_no_args(&self) -> bool {
self.allow_no_args
}
#[doc(hidden)]
/// Called by generated code, do not call directly.
pub fn register_callback(&mut self, callback: Callback) {
self.callback = Some(callback);
}
#[doc(hidden)]
/// Called by generated code, do not call directly.
/// # Safety
/// This function is unsafe because it interacts with the C API.
pub unsafe fn handle_call_context(&mut self, args: *mut *mut i8, count: libc::c_int) {
self.context_manager
.replace(Some(ArmaCallContext::from_arma(args, count)));
}
#[must_use]
/// Get a context for interacting with Arma
pub fn context(&self) -> Context {
Context::new(
self.callback_channel.0.clone(),
GlobalContext::new(self.version.clone(), self.group.state.clone()),
GroupContext::new(self.group.state.clone()),
)
}
#[doc(hidden)]
/// Called by generated code, do not call directly.
/// # Safety
/// This function is unsafe because it interacts with the C API.
pub unsafe fn handle_call(
&self,
function: *mut libc::c_char,
output: *mut libc::c_char,
size: libc::size_t,
args: Option<*mut *mut i8>,
count: Option<libc::c_int>,
clear_call_context: bool,
) -> libc::c_int {
if clear_call_context && !self.pre218_clear_context_override {
self.context_manager.replace(None);
}
let function = if let Ok(cstring) = std::ffi::CStr::from_ptr(function).to_str() {
cstring.to_string()
} else {
return 1;
};
match function.as_str() {
#[cfg(windows)]
"::console" => {
if !CONSOLE_ALLOCATED.swap(true, std::sync::atomic::Ordering::SeqCst) {
let _ = windows::Win32::System::Console::AllocConsole();
}
0
}
_ => self.group.handle(
self.context().with_buffer_size(size),
self.context_manager.as_ref(),
&function,
output,
size,
args,
count,
),
}
}
#[must_use]
/// Create a version of the extension that can be used in tests.
pub fn testing(self) -> testing::Extension {
testing::Extension::new(self)
}
#[doc(hidden)]
/// Called by generated code, do not call directly.
pub fn run_callbacks(&mut self) {
let callback = self.callback;
let (_, rx) = self.callback_channel.clone();
self.callback_thread = Some(std::thread::spawn(move || {
while let Ok(CallbackMessage::Call(name, func, data)) = rx.recv() {
if let Some(c) = callback {
let name = if let Ok(cstring) = std::ffi::CString::new(name) {
cstring
} else {
error!("callback name was not valid");
continue;
};
let func = if let Ok(cstring) = std::ffi::CString::new(func) {
cstring
} else {
error!("callback func was not valid");
continue;
};
let data = if let Ok(cstring) = std::ffi::CString::new(match data {
Some(value) => match value {
Value::String(s) => s,
v => v.to_string(),
},
None => String::new(),
}) {
cstring
} else {
error!("callback data was not valid");
continue;
};
let (name, func, data) = (name.into_raw(), func.into_raw(), data.into_raw());
loop {
if c(name, func, data) >= 0 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
unsafe {
drop(std::ffi::CString::from_raw(name));
drop(std::ffi::CString::from_raw(func));
drop(std::ffi::CString::from_raw(data));
}
}
}
}));
}
}
#[cfg(feature = "extension")]
impl Drop for Extension {
// Never called when loaded by arma, instead this is purely required for rust testing.
fn drop(&mut self) {
if let Some(thread) = self.callback_thread.take() {
let (tx, _) = &self.callback_channel;
tx.send(CallbackMessage::Terminate).unwrap();
thread.join().unwrap();
}
}
}
/// Used to build an extension.
#[cfg(feature = "extension")]
pub struct ExtensionBuilder {
version: String,
group: Group,
allow_no_args: bool,
}
#[cfg(feature = "extension")]
impl ExtensionBuilder {
#[inline]
#[must_use]
/// Sets the version of the extension.
pub fn version(mut self, version: String) -> Self {
self.version = version;
self
}
#[inline]
#[must_use]
/// Add a group to the extension.
pub fn group<S>(mut self, name: S, group: Group) -> Self
where
S: Into<String>,
{
self.group = self.group.group(name.into(), group);
self
}
#[inline]
#[must_use]
/// Add a new state value to the extension if it has not be added already
pub fn state<T>(mut self, state: T) -> Self
where
T: Send + Sync + 'static,
{
self.group = self.group.state(state);
self
}
#[inline]
#[must_use]
/// Freeze the extension's state, preventing the state from changing, allowing for faster reads
pub fn freeze_state(mut self) -> Self {
self.group = self.group.freeze_state();
self
}
#[inline]
#[must_use]
/// Allows the extension to be called without any arguments.
/// Example:
/// ```sqf
/// "my_ext" callExtension "my_func"
/// ```
pub const fn allow_no_args(mut self) -> Self {
self.allow_no_args = true;
self
}
#[inline]
#[must_use]
/// Add a command to the extension.
pub fn command<S, F, I, R>(mut self, name: S, handler: F) -> Self
where
S: Into<String>,
F: Factory<I, R> + 'static,
{
self.group = self.group.command(name, handler);
self
}
#[inline]
#[must_use]
/// Builds the extension.
pub fn finish(self) -> Extension {
#[expect(unused_mut, reason = "Only used on Windows release")]
let mut pre218 = false;
#[allow(unused_variables)]
let function_name =
std::ffi::CString::new("RVExtensionRequestContext").expect("CString::new failed");
#[cfg(all(windows, not(debug_assertions)))]
let request_context: ContextRequest = {
let handle = unsafe { winapi::um::libloaderapi::GetModuleHandleW(std::ptr::null()) };
if handle.is_null() {
panic!("GetModuleHandleW failed");
}
let func_address =
unsafe { winapi::um::libloaderapi::GetProcAddress(handle, function_name.as_ptr()) };
if func_address.is_null() {
pre218 = true;
empty_request_context
} else {
unsafe { std::mem::transmute(func_address) }
}
};
#[cfg(all(not(windows), not(debug_assertions)))]
let request_context: ContextRequest = {
let handle = unsafe { libc::dlopen(std::ptr::null(), libc::RTLD_LAZY) };
if handle.is_null() {
panic!("Failed to open handle to current process");
}
let func_address = unsafe { libc::dlsym(handle, function_name.as_ptr()) };
if func_address.is_null() {
pre218 = true;
empty_request_context
} else {
let func = unsafe { std::mem::transmute(func_address) };
unsafe { libc::dlclose(handle) };
func
}
};
#[cfg(debug_assertions)]
let request_context = empty_request_context;
Extension {
version: self.version,
group: self.group.into(),
allow_no_args: self.allow_no_args,
callback: None,
callback_channel: unbounded(),
callback_thread: None,
context_manager: Rc::new(ArmaContextManager::new(request_context)),
pre218_clear_context_override: pre218,
}
}
}
unsafe extern "C" fn empty_request_context() {}
#[doc(hidden)]
/// Called by generated code, do not call directly.
///
/// # Safety
/// This function is unsafe because it interacts with the C API.
///
/// # Note
/// This function assumes `buf_size` includes space for a single terminating zero byte at the end.
#[cfg(feature = "extension")]
pub unsafe fn write_cstr(
string: String,
ptr: *mut libc::c_char,
buf_size: libc::size_t,
) -> Option<libc::size_t> {
if string.is_empty() {
return Some(0);
}
let cstr = std::ffi::CString::new(string).ok()?;
let len_to_copy = cstr.as_bytes().len();
if len_to_copy >= buf_size {
return None;
}
ptr.copy_from(cstr.as_ptr(), len_to_copy);
ptr.add(len_to_copy).write(0x00);
Some(len_to_copy)
}
#[cfg(all(test, feature = "extension"))]
mod tests {
use super::*;
#[test]
fn write_size_zero() {
const BUF_SIZE: libc::size_t = 0;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("a".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, None);
assert_eq!(buf, [0; BUF_SIZE]);
}
#[test]
fn write_size_zero_empty() {
const BUF_SIZE: libc::size_t = 0;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, Some(0));
assert_eq!(buf, [0; BUF_SIZE]);
}
#[test]
fn write_size_one() {
const BUF_SIZE: libc::size_t = 1;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("a".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, None);
assert_eq!(buf, [0; BUF_SIZE]);
}
#[test]
fn write_size_one_empty() {
const BUF_SIZE: libc::size_t = 1;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, Some(0));
assert_eq!(buf, [0; BUF_SIZE]);
}
#[test]
fn write_empty() {
const BUF_SIZE: libc::size_t = 7;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, Some(0));
assert_eq!(buf, [0; BUF_SIZE]);
}
#[test]
fn write_half() {
const BUF_SIZE: libc::size_t = 7;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("foo".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, Some(3));
assert_eq!(buf, (b"foo\0\0\0\0").map(|c| c as i8));
}
#[test]
fn write_full() {
const BUF_SIZE: libc::size_t = 7;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("foobar".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, Some(6));
assert_eq!(buf, (b"foobar\0").map(|c| c as i8));
}
#[test]
fn write_overflow() {
const BUF_SIZE: libc::size_t = 7;
let mut buf = [0; BUF_SIZE];
let result = unsafe { write_cstr("foo bar".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, None);
assert_eq!(buf, [0; BUF_SIZE]);
}
#[test]
fn write_overwrite() {
const BUF_SIZE: libc::size_t = 7;
let mut buf = (b"zzzzzz\0").map(|c| c as i8);
let result = unsafe { write_cstr("a".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
assert_eq!(result, Some(1));
assert_eq!(buf, (b"a\0zzzz\0").map(|c| c as i8));
}
}

167
vendor/arma-rs/src/testing.rs vendored Normal file
View File

@@ -0,0 +1,167 @@
//! For testing your extension.
use std::time::Duration;
use crate::{CallbackMessage, Context, State, Value};
use crate::{ArmaCallContext, Caller, Mission, Server, Source};
/// Wrapper around [`crate::Extension`] used for testing.
pub struct Extension(crate::Extension);
const BUFFER_SIZE: libc::size_t = 10240; // The sized used by Arma 3 as of 2021-12-30
#[derive(Debug, PartialEq, Eq)]
/// Result of an event handler
pub enum Result<T, E> {
/// an event has been handled and the handler is done, the value of T is the return value of the event handler
Ok(T),
/// the handler has encountered an error, the value of T is the return value of the event handler
Err(E),
/// an event is handled but the handler is not done and should receive another event
Continue,
/// the handler reached the specified timeout
Timeout,
}
impl<T, E> Result<T, E> {
/// Returns true if the result is an ok result
pub fn is_ok(&self) -> bool {
matches!(self, Self::Ok(_))
}
/// Returns true if the result is an error
pub fn is_err(&self) -> bool {
matches!(self, Self::Err(_))
}
/// Returns true if the result is a continue result
pub fn is_continue(&self) -> bool {
matches!(self, Self::Continue)
}
/// Returns true if the result is a timeout result
pub fn is_timeout(&self) -> bool {
matches!(self, Self::Timeout)
}
}
impl Extension {
/// Create a new testing Extension
pub fn new(ext: crate::Extension) -> Self {
Self(ext)
}
#[must_use]
/// Returns a context for simulating interactions with Arma
pub fn context(&self) -> Context {
self.0.context().with_buffer_size(BUFFER_SIZE)
}
#[must_use]
/// Get a reference to the extensions state container
pub fn state(&self) -> &State {
&self.0.group.state
}
#[must_use]
#[allow(clippy::too_many_arguments)]
/// Call a function with Arma call context.
///
/// # Safety
/// This function is unsafe because it interacts with the C API.
pub fn call_with_context(
&self,
function: &str,
args: Option<Vec<String>>,
caller: Caller,
source: Source,
mission: Mission,
server: Server,
remote_exec_owner: i16,
) -> (String, libc::c_int) {
self.0.context_manager.replace(Some(ArmaCallContext::new(
caller,
source,
mission,
server,
remote_exec_owner,
)));
unsafe { self.handle_call(function, args) }
}
#[must_use]
/// Call a function without Arma call context.
///
/// # Safety
/// This function is unsafe because it interacts with the C API.
///
/// # Note
/// If the `call-context` feature is enabled, this function passes default values for each field.
pub fn call(&self, function: &str, args: Option<Vec<String>>) -> (String, libc::c_int) {
self.0.context_manager.replace(None);
unsafe { self.handle_call(function, args) }
}
unsafe fn handle_call(
&self,
function: &str,
args: Option<Vec<String>>,
) -> (String, libc::c_int) {
let mut output = [0; BUFFER_SIZE];
let len = args.as_ref().map(|a| a.len().try_into().unwrap());
let mut args_pointer = args.map(|v| {
v.into_iter()
.map(|s| std::ffi::CString::new(s).unwrap().into_raw())
.collect::<Vec<*mut i8>>()
});
let res = self.0.group.handle(
self.context(),
&self.0.context_manager,
function,
output.as_mut_ptr(),
BUFFER_SIZE,
args_pointer.as_mut().map(Vec::as_mut_ptr),
len,
);
if let Some(args) = args_pointer {
for arg in args {
let _ = std::ffi::CString::from_raw(arg);
}
}
(
std::ffi::CStr::from_ptr(output.as_ptr())
.to_str()
.unwrap()
.to_string(),
res,
)
}
/// Create a callback handler
///
/// Returns a Result from the handler if the callback was handled,
/// or `Result::Timeout` if either no event was received, or the handler
/// returned `Result::Continue` until the timeout was reached.
///
/// The handler must return a Result indicating the callback was handled to exit
/// `Result::Continue` will continue to provide events to the handler until another variant is returned
pub fn callback_handler<F, T, E>(&self, handler: F, timeout: Duration) -> Result<T, E>
where
F: Fn(&str, &str, Option<Value>) -> Result<T, E>,
{
let (_, rx) = &self.0.callback_channel;
let deadline = std::time::Instant::now() + timeout;
loop {
match rx.recv_deadline(deadline) {
Ok(CallbackMessage::Call(name, func, data)) => match handler(&name, &func, data) {
Result::Ok(value) => return Result::Ok(value),
Result::Err(error) => return Result::Err(error),
Result::Timeout => return Result::Timeout,
Result::Continue => {}
},
_ => return Result::Timeout,
}
}
}
}

View File

@@ -0,0 +1,41 @@
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, TimeZone, Timelike};
use crate::{FromArma, FromArmaError, IntoArma, Value};
impl IntoArma for NaiveDateTime {
fn to_arma(&self) -> Value {
vec![
self.year() as u32,
self.month(),
self.day(),
self.hour(),
self.minute(),
self.second(),
self.nanosecond() / 1_000_000,
]
.to_arma()
}
}
impl<T: TimeZone> IntoArma for DateTime<T> {
fn to_arma(&self) -> Value {
self.naive_utc().to_arma()
}
}
impl FromArma for NaiveDateTime {
fn from_arma(s: String) -> Result<Self, FromArmaError> {
let arma_date: [i64; 7] = FromArma::from_arma(s)?;
Ok(NaiveDate::from_ymd(
arma_date[0].try_into().unwrap(),
arma_date[1].try_into().unwrap(),
arma_date[2].try_into().unwrap(),
)
.and_hms_milli(
arma_date[3].try_into().unwrap(),
arma_date[4].try_into().unwrap(),
arma_date[5].try_into().unwrap(),
arma_date[6].try_into().unwrap(),
))
}
}

View File

@@ -0,0 +1,8 @@
#[cfg(feature = "uuid")]
mod uuid;
#[cfg(feature = "chrono")]
mod chrono;
#[cfg(feature = "serde_json")]
mod serde_json;

View File

@@ -0,0 +1,51 @@
use crate::{FromArma, FromArmaError, IntoArma, Value};
impl IntoArma for serde_json::Value {
fn to_arma(&self) -> Value {
match self {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Boolean(*b),
serde_json::Value::Number(n) => Value::Number(if n.is_f64() {
n.as_f64().unwrap()
} else if n.is_i64() {
n.as_i64().unwrap() as f64
} else if n.is_u64() {
n.as_u64().unwrap() as f64
} else {
unreachable!()
}),
serde_json::Value::String(s) => Value::String(s.to_owned()),
serde_json::Value::Array(v) => {
Value::Array(v.iter().map(|v| v.to_arma()).collect::<Vec<Value>>())
}
serde_json::Value::Object(o) => o
.iter()
.map(|(k, v)| vec![Value::String(k.to_owned()), v.to_arma()])
.collect::<Vec<Vec<Value>>>()
.to_arma(),
}
}
}
impl FromArma for serde_json::Value {
fn from_arma(s: String) -> Result<Self, FromArmaError> {
let value = Value::from_arma(s)?;
Ok(value.to_json())
}
}
impl Value {
/// Convert a Value to a serde_json::Value
pub fn to_json(&self) -> serde_json::Value {
match self {
Value::Null => serde_json::Value::Null,
Value::Boolean(b) => serde_json::Value::Bool(*b),
Value::Number(n) => {
serde_json::Value::Number(serde_json::Number::from_f64(*n).unwrap())
}
Value::String(s) => serde_json::Value::String(s.to_owned()),
Value::Array(v) => serde_json::Value::Array(v.iter().map(|v| v.to_json()).collect()),
Value::Unknown(s) => serde_json::Value::String(s.to_owned()),
}
}
}

Some files were not shown because too many files have changed in this diff Show More