use std::collections::VecDeque;
use crate::bytes::Reader;
use crate::error::{Error, Result};
use crate::message::{
self, HEADER_LEN, NAT_CONNECT, NAT_DISCONNECT, NAT_KEEPALIVE, NAT_REQUEST,
NAT_REQUEST_FRAMEOFDATA, NAT_REQUEST_MODELDEF, NAT_RESPONSE,
};
use crate::serverinfo::ServerInfo;
use crate::version::Version;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommandComponent {
Node,
Property,
Value,
Authority,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ComponentError {
Empty,
Comma,
Nul,
Control(u8),
NonAscii(u8),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
bytes: Vec<u8>,
metadata: RequestMetadata,
}
impl Request {
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
#[cfg(feature = "net")]
pub(crate) fn kind(&self) -> RequestKind {
self.metadata.kind
}
#[cfg(feature = "net")]
pub(crate) fn response_kind(&self) -> ResponseKind {
self.metadata.response
}
#[cfg(feature = "net")]
pub(crate) fn tier(&self) -> Option<WriteTier> {
self.metadata.tier
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct RequestMetadata {
kind: RequestKind,
response: ResponseKind,
target: Option<String>,
property: Option<String>,
value: Option<PropertyValue>,
tier: Option<WriteTier>,
authority: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RequestKind {
Connect,
ModelDefinition,
FrameOfData,
GetProperty,
SetProperty,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResponseKind {
ServerInfo,
ModelDefinition,
FrameOfData,
Status,
Value,
}
pub fn connect_request() -> core::result::Result<Request, CommandError> {
Ok(empty_request(
NAT_CONNECT,
RequestKind::Connect,
ResponseKind::ServerInfo,
))
}
pub const CONNECT_SENDER_LEN: usize = 264;
pub const CONNECT_OPTIONS_PAYLOAD_LEN: usize = CONNECT_SENDER_LEN + 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ConnectOptions {
pub subscribed_data_only: bool,
pub bitstream_version: Option<Version>,
}
impl ConnectOptions {
#[must_use]
pub const fn requesting(version: Version) -> Self {
Self {
subscribed_data_only: false,
bitstream_version: Some(version),
}
}
#[must_use]
pub fn parse(payload: &[u8]) -> Option<Self> {
let flag = *payload.get(CONNECT_SENDER_LEN)?;
let version = payload.get(CONNECT_SENDER_LEN + 1..CONNECT_SENDER_LEN + 5)?;
let version = Version::from_bytes(version.try_into().ok()?);
Some(Self {
subscribed_data_only: flag != 0,
bitstream_version: (version != Version::new(0, 0, 0, 0)).then_some(version),
})
}
#[must_use]
pub fn stream_version(&self, info: &ServerInfo) -> Version {
match self.bitstream_version {
Some(requested) if info.protocol_version.major >= 3 => requested,
_ => info.protocol_version,
}
}
}
pub fn connect_request_with(
options: ConnectOptions,
) -> core::result::Result<Request, CommandError> {
let mut payload = [0u8; CONNECT_OPTIONS_PAYLOAD_LEN];
payload[CONNECT_SENDER_LEN] = u8::from(options.subscribed_data_only);
if let Some(version) = options.bitstream_version {
payload[CONNECT_SENDER_LEN + 1..CONNECT_SENDER_LEN + 5]
.copy_from_slice(&version.to_bytes());
}
let mut request = empty_request(NAT_CONNECT, RequestKind::Connect, ResponseKind::ServerInfo);
request.bytes = message::frame(NAT_CONNECT, &payload);
Ok(request)
}
pub fn request_modeldef() -> core::result::Result<Request, CommandError> {
Ok(empty_request(
NAT_REQUEST_MODELDEF,
RequestKind::ModelDefinition,
ResponseKind::ModelDefinition,
))
}
pub fn request_frame_of_data() -> core::result::Result<Request, CommandError> {
Ok(empty_request(
NAT_REQUEST_FRAMEOFDATA,
RequestKind::FrameOfData,
ResponseKind::FrameOfData,
))
}
#[must_use]
pub const fn keepalive_datagram() -> [u8; HEADER_LEN] {
empty_datagram(NAT_KEEPALIVE)
}
#[must_use]
pub const fn disconnect_datagram() -> [u8; HEADER_LEN] {
empty_datagram(NAT_DISCONNECT)
}
const fn empty_datagram(message_id: u16) -> [u8; HEADER_LEN] {
let id = message_id.to_le_bytes();
[id[0], id[1], 0, 0]
}
fn empty_request(message_id: u16, kind: RequestKind, response: ResponseKind) -> Request {
Request {
bytes: message::frame(message_id, &[]),
metadata: RequestMetadata {
kind,
response,
target: None,
property: None,
value: None,
tier: None,
authority: None,
},
}
}
pub fn get_property(node: &str, property: &str) -> core::result::Result<Request, CommandError> {
validate_component(node, CommandComponent::Node, true)?;
let property = canonical_property(property)?;
let payload = command_payload(&["GetProperty", node, &property])?;
Ok(request_from_payload(
&payload,
RequestMetadata {
kind: RequestKind::GetProperty,
response: ResponseKind::Value,
target: Some(node.to_owned()),
property: Some(property),
value: None,
tier: None,
authority: None,
},
))
}
fn command_payload(components: &[&str]) -> core::result::Result<Vec<u8>, CommandError> {
let mut payload = Vec::new();
for (index, component) in components.iter().enumerate() {
if index != 0 {
payload.push(b',');
}
payload.extend_from_slice(component.as_bytes());
}
payload.push(0);
if payload.len() > usize::from(u16::MAX) {
return Err(CommandError::RequestTooLong {
length: payload.len(),
max: usize::from(u16::MAX),
});
}
Ok(payload)
}
fn request_from_payload(payload: &[u8], metadata: RequestMetadata) -> Request {
Request {
bytes: message::frame(NAT_REQUEST, payload),
metadata,
}
}
fn validate_component(
value: &str,
component: CommandComponent,
allow_empty: bool,
) -> core::result::Result<(), CommandError> {
if value.is_empty() && !allow_empty {
return Err(CommandError::InvalidComponent {
component,
reason: ComponentError::Empty,
});
}
for &byte in value.as_bytes() {
if !byte.is_ascii() {
return Err(CommandError::InvalidComponent {
component,
reason: ComponentError::NonAscii(byte),
});
}
match byte {
b',' => {
return Err(CommandError::InvalidComponent {
component,
reason: ComponentError::Comma,
});
}
0 => {
return Err(CommandError::InvalidComponent {
component,
reason: ComponentError::Nul,
});
}
0..=0x1f | 0x7f => {
return Err(CommandError::InvalidComponent {
component,
reason: ComponentError::Control(byte),
});
}
_ => {}
}
}
Ok(())
}
fn validate_authority(authority: &str) -> core::result::Result<(), CommandError> {
validate_component(authority, CommandComponent::Authority, false)?;
if authority.trim().is_empty() {
return Err(CommandError::InvalidComponent {
component: CommandComponent::Authority,
reason: ComponentError::Empty,
});
}
Ok(())
}
fn canonical_property(property: &str) -> core::result::Result<String, CommandError> {
validate_component(property, CommandComponent::Property, false)?;
let trimmed = property.trim();
if trimmed.is_empty() {
return Err(CommandError::InvalidComponent {
component: CommandComponent::Property,
reason: ComponentError::Empty,
});
}
for candidate in TIER1_PROPERTIES {
if trimmed.eq_ignore_ascii_case(candidate) {
return Ok(candidate.to_owned());
}
}
for candidate in Tier2Property::ALL {
if trimmed.eq_ignore_ascii_case(candidate.property_name()) {
return Ok(candidate.property_name().to_owned());
}
}
Ok(trimmed.to_owned())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PropertyValue {
Bool(bool),
Int(i64),
Text(String),
}
impl PropertyValue {
#[must_use]
pub fn wire(&self) -> String {
match self {
Self::Bool(value) => i32::from(*value).to_string(),
Self::Int(value) => value.to_string(),
Self::Text(value) => value.clone(),
}
}
fn validate(&self) -> core::result::Result<String, CommandError> {
let wire = self.wire();
validate_component(&wire, CommandComponent::Value, true)?;
Ok(wire)
}
#[must_use]
pub fn matches(&self, echo: &str) -> bool {
match self {
Self::Bool(value) => {
let echo = echo.trim();
let truthy = echo == "1" || echo.eq_ignore_ascii_case("true");
let falsy = echo == "0" || echo.eq_ignore_ascii_case("false");
(*value && truthy) || (!*value && falsy)
}
Self::Int(value) => echo
.trim()
.parse::<i64>()
.is_ok_and(|found| found == *value),
Self::Text(value) => echo == value,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WriteTier {
Tier1,
Tier2,
Unsanctioned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Tier2Property {
TransmissionType,
LocalInterface,
Port,
LiveEditMode,
Recording,
Calibration,
AssetDefinition,
}
impl Tier2Property {
pub const ALL: [Self; 7] = [
Self::TransmissionType,
Self::LocalInterface,
Self::Port,
Self::LiveEditMode,
Self::Recording,
Self::Calibration,
Self::AssetDefinition,
];
#[must_use]
pub const fn property_name(self) -> &'static str {
match self {
Self::TransmissionType => "Transmission Type",
Self::LocalInterface => "Local Interface",
Self::Port => "Port",
Self::LiveEditMode => "Mode",
Self::Recording => "Record",
Self::Calibration => "Calibration",
Self::AssetDefinition => "Asset Definition",
}
}
}
const TIER1_PROPERTIES: [&str; 3] = ["Enabled", "Video Mode", "Streaming"];
#[must_use]
pub fn classify_property(property: &str) -> WriteTier {
let property = property.trim();
if TIER1_PROPERTIES
.iter()
.any(|candidate| property.eq_ignore_ascii_case(candidate))
{
return WriteTier::Tier1;
}
if Tier2Property::ALL
.iter()
.any(|candidate| property.eq_ignore_ascii_case(candidate.property_name()))
{
return WriteTier::Tier2;
}
WriteTier::Unsanctioned
}
fn build_set_request(
target: &str,
property: &str,
value: &PropertyValue,
tier: WriteTier,
authority: Option<String>,
) -> core::result::Result<Request, CommandError> {
validate_component(target, CommandComponent::Node, true)?;
let property = canonical_property(property)?;
let wire = value.validate()?;
let payload = command_payload(&["SetProperty", target, &property, &wire])?;
Ok(request_from_payload(
&payload,
RequestMetadata {
kind: RequestKind::SetProperty,
response: ResponseKind::Status,
target: Some(target.to_owned()),
property: Some(property),
value: Some(value.clone()),
tier: Some(tier),
authority,
},
))
}
#[derive(Debug)]
pub struct Tier2Grant {
authority: String,
action: Tier2Property,
}
impl Tier2Grant {
pub fn sign_off(
authority: impl Into<String>,
action: Tier2Property,
) -> core::result::Result<Self, CommandError> {
let authority = authority.into();
validate_authority(&authority)?;
Ok(Self { authority, action })
}
#[must_use]
pub fn authority(&self) -> &str {
&self.authority
}
#[must_use]
pub const fn action(&self) -> Tier2Property {
self.action
}
}
#[derive(Debug)]
pub struct Tier2Write {
request: Request,
authority: String,
action: Tier2Property,
}
impl Tier2Write {
#[allow(clippy::needless_pass_by_value)]
pub fn authorize(
grant: Tier2Grant,
node: impl Into<String>,
value: PropertyValue,
) -> core::result::Result<Self, CommandError> {
let Tier2Grant { authority, action } = grant;
if matches!(action, Tier2Property::AssetDefinition) {
return Err(CommandError::UnsupportedOperation {
operation: "Asset Definition",
});
}
let node = node.into();
let request = build_set_request(
&node,
action.property_name(),
&value,
WriteTier::Tier2,
Some(authority.clone()),
)?;
Ok(Self {
request,
authority,
action,
})
}
#[must_use]
pub fn request(&self) -> &Request {
&self.request
}
#[must_use]
pub fn into_request(self) -> Request {
self.request
}
#[must_use]
pub fn authority(&self) -> &str {
&self.authority
}
#[must_use]
pub const fn action(&self) -> Tier2Property {
self.action
}
}
struct PlannedTarget {
target: String,
set_request: Request,
reread_request: Request,
}
impl core::fmt::Debug for PlannedTarget {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter
.debug_struct("PlannedTarget")
.field("target", &self.target)
.field("set_request", &self.set_request)
.field("reread_request", &self.reread_request)
.finish()
}
}
#[derive(Debug)]
pub struct Tier1Write {
property: String,
value: PropertyValue,
first: PlannedTarget,
targets: VecDeque<PlannedTarget>,
}
impl Tier1Write {
pub fn camera_enabled(
serials: Vec<String>,
enabled: bool,
) -> core::result::Result<Self, CommandError> {
Self::new(serials, "Enabled", PropertyValue::Bool(enabled))
}
pub fn camera_video_mode(
serials: Vec<String>,
mode: i64,
) -> core::result::Result<Self, CommandError> {
Self::new(serials, "Video Mode", PropertyValue::Int(mode))
}
pub fn asset_streaming(
assets: Vec<String>,
streaming: bool,
) -> core::result::Result<Self, CommandError> {
Self::new(assets, "Streaming", PropertyValue::Bool(streaming))
}
fn new(
targets: Vec<String>,
property: &str,
value: PropertyValue,
) -> core::result::Result<Self, CommandError> {
if targets.is_empty() {
return Err(CommandError::NoTargets);
}
let property = canonical_property(property)?;
if classify_property(&property) != WriteTier::Tier1 {
return Err(CommandError::Tier2Refused { property });
}
value.validate()?;
for target in &targets {
validate_component(target, CommandComponent::Node, false)?;
}
for (index, target) in targets.iter().enumerate() {
if targets[index + 1..].iter().any(|other| other == target) {
return Err(CommandError::DuplicateTarget {
target: target.clone(),
});
}
}
let mut planned = VecDeque::with_capacity(targets.len());
for target in targets {
let set_request =
build_set_request(&target, &property, &value, WriteTier::Tier1, None)?;
let reread_request = get_property(&target, &property)?;
planned.push_back(PlannedTarget {
target,
set_request,
reread_request,
});
}
let Some(first) = planned.pop_front() else {
return Err(CommandError::NoTargets);
};
Ok(Self {
property,
value,
first,
targets: planned,
})
}
#[must_use]
pub fn property(&self) -> &str {
&self.property
}
#[must_use]
pub fn value(&self) -> &PropertyValue {
&self.value
}
#[must_use]
pub fn begin(self) -> CanaryStep {
CanaryStep {
current: self.first,
remaining: self.targets,
property: self.property,
value: self.value,
canary: true,
progress: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct CanaryStep {
current: PlannedTarget,
remaining: VecDeque<PlannedTarget>,
property: String,
value: PropertyValue,
canary: bool,
progress: Vec<ProgressRecord>,
}
impl CanaryStep {
#[must_use]
pub fn target(&self) -> &str {
&self.current.target
}
#[must_use]
pub const fn is_canary(&self) -> bool {
self.canary
}
#[must_use]
pub fn set_request(&self) -> &Request {
&self.current.set_request
}
#[must_use]
pub fn reread_request(&self) -> &Request {
&self.current.reread_request
}
pub fn verify_echo(self, echo: &str) -> core::result::Result<Verified, CommandError> {
self.finish(None, echo)
}
pub fn verify(self, reread_reply: &[u8]) -> core::result::Result<Verified, CommandError> {
let response = match CommandResponse::parse(reread_reply) {
Ok(response) => response,
Err(error) => return Err(self.failure(CommandError::Wire(error))),
};
match response {
CommandResponse::Value(value) => self.verify_echo(&value),
CommandResponse::Status(_) => Err(self.failure(CommandError::UnverifiableReply)),
}
}
pub fn verify_status(
self,
status_reply: &[u8],
reread_reply: &[u8],
) -> core::result::Result<Verified, CommandError> {
let response = match CommandResponse::parse(status_reply) {
Ok(response) => response,
Err(error) => return Err(self.failure(CommandError::Wire(error))),
};
let status = match response {
CommandResponse::Status(status) => status,
CommandResponse::Value(_) => return Err(self.failure(CommandError::StatusExpected)),
};
if status != 0 {
let target = self.current.target.clone();
return Err(self.failure(CommandError::StatusFailure { target, status }));
}
let echo = match CommandResponse::parse_value(reread_reply) {
Ok(echo) => echo,
Err(error) => return Err(self.failure(CommandError::Wire(error))),
};
self.finish(Some(status), &echo)
}
fn failure(self, source: CommandError) -> CommandError {
if self.progress.is_empty() {
source
} else {
CommandError::VerificationFailed {
source: Box::new(source),
progress: self.progress,
}
}
}
fn finish(
self,
status: Option<i32>,
echo: &str,
) -> core::result::Result<Verified, CommandError> {
if !self.value.matches(echo) {
let target = self.current.target.clone();
let property = self.property.clone();
let expected = self.value.wire();
return Err(self.failure(CommandError::RereadMismatch {
target,
property,
expected,
found: echo.to_owned(),
}));
}
let mut progress = self.progress;
progress.push(ProgressRecord {
target: self.current.target,
phase: if self.canary {
ProgressPhase::Canary
} else {
ProgressPhase::Target
},
status,
reread: echo.to_owned(),
});
Ok(Verified {
remaining: self.remaining,
property: self.property,
value: self.value,
progress,
})
}
}
#[derive(Debug)]
pub struct Verified {
remaining: VecDeque<PlannedTarget>,
property: String,
value: PropertyValue,
progress: Vec<ProgressRecord>,
}
impl Verified {
#[must_use]
pub fn next(self) -> Option<CanaryStep> {
let mut remaining = self.remaining;
let current = remaining.pop_front()?;
Some(CanaryStep {
current,
remaining,
property: self.property,
value: self.value,
canary: false,
progress: self.progress,
})
}
#[must_use]
pub fn progress(&self) -> &[ProgressRecord] {
&self.progress
}
pub fn finish(self) -> core::result::Result<ProgressLog, CommandError> {
if !self.remaining.is_empty() {
return Err(CommandError::UnfinishedTargets {
remaining: self.remaining.len(),
});
}
Ok(ProgressLog {
records: self.progress,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgressLog {
records: Vec<ProgressRecord>,
}
impl ProgressLog {
#[must_use]
pub fn records(&self) -> &[ProgressRecord] {
&self.records
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgressRecord {
target: String,
phase: ProgressPhase,
status: Option<i32>,
reread: String,
}
impl ProgressRecord {
#[must_use]
pub fn target(&self) -> &str {
&self.target
}
#[must_use]
pub const fn phase(&self) -> ProgressPhase {
self.phase
}
#[must_use]
pub const fn status(&self) -> Option<i32> {
self.status
}
#[must_use]
pub fn reread(&self) -> &str {
&self.reread
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgressPhase {
Canary,
Target,
}
#[derive(Debug)]
pub struct SetProperty;
impl SetProperty {
pub fn plan(
nodes: Vec<String>,
property: &str,
value: PropertyValue,
) -> core::result::Result<Tier1Write, CommandError> {
let property = canonical_property(property)?;
match classify_property(&property) {
WriteTier::Tier1 => Tier1Write::new(nodes, &property, value),
WriteTier::Tier2 | WriteTier::Unsanctioned => {
Err(CommandError::Tier2Refused { property })
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CommandResponse {
Status(i32),
Value(String),
}
impl CommandResponse {
pub fn parse(datagram: &[u8]) -> Result<Self> {
Self::parse_payload(message::expect(datagram, NAT_RESPONSE)?)
}
pub fn parse_payload(payload: &[u8]) -> Result<Self> {
if payload.len() == 4 {
let mut reader = Reader::new(payload);
return Ok(Self::Status(reader.i32()?));
}
Self::parse_value_payload(payload).map(Self::Value)
}
pub fn parse_value(datagram: &[u8]) -> Result<String> {
Self::parse_value_payload(message::expect(datagram, NAT_RESPONSE)?)
}
pub fn parse_value_payload(payload: &[u8]) -> Result<String> {
Reader::new(payload).fixed_cstr(payload.len())
}
#[must_use]
pub fn status_datagram(status: i32) -> Vec<u8> {
message::frame(NAT_RESPONSE, &status.to_le_bytes())
}
#[must_use]
pub fn value_datagram(value: &str) -> Vec<u8> {
let mut payload = value.as_bytes().to_vec();
payload.push(0);
message::frame(NAT_RESPONSE, &payload)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CommandError {
InvalidComponent {
component: CommandComponent,
reason: ComponentError,
},
RequestTooLong {
length: usize,
max: usize,
},
DuplicateTarget {
target: String,
},
NoTargets,
UnsupportedOperation {
operation: &'static str,
},
Tier2Refused {
property: String,
},
StatusFailure {
target: String,
status: i32,
},
StatusExpected,
RereadMismatch {
target: String,
property: String,
expected: String,
found: String,
},
VerificationFailed {
source: Box<Self>,
progress: Vec<ProgressRecord>,
},
UnverifiableReply,
Wire(Error),
UnfinishedTargets {
remaining: usize,
},
}
impl CommandError {
#[must_use]
pub fn progress(&self) -> &[ProgressRecord] {
match self {
Self::VerificationFailed { progress, .. } => progress,
_ => &[],
}
}
}
impl core::fmt::Display for CommandError {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::InvalidComponent { component, reason } => {
write!(
formatter,
"invalid {component:?} command component: {reason:?}"
)
}
Self::RequestTooLong { length, max } => {
write!(formatter, "command payload length {length} exceeds {max}")
}
Self::DuplicateTarget { target } => write!(formatter, "duplicate target {target:?}"),
Self::NoTargets => formatter.write_str("write named no target units"),
Self::UnsupportedOperation { operation } => {
write!(
formatter,
"unsupported NatNet command operation {operation:?}"
)
}
Self::Tier2Refused { property } => write!(
formatter,
"Tier-2 or unsanctioned property {property:?} needs an explicit Tier2Grant"
),
Self::StatusFailure { target, status } => {
write!(
formatter,
"target {target:?} returned SetProperty status {status}"
)
}
Self::StatusExpected => formatter.write_str("SetProperty reply was not a status"),
Self::RereadMismatch {
target,
property,
expected,
found,
} => write!(
formatter,
"target {target:?} re-read mismatch on {property:?}: wrote {expected:?}, read {found:?}"
),
Self::VerificationFailed { source, progress } => write!(
formatter,
"Tier-1 verification failed after {} completed target(s): {source}",
progress.len()
),
Self::UnverifiableReply => {
formatter.write_str("canary re-read reply was a status, not a value")
}
Self::Wire(error) => write!(formatter, "reply decode error: {error}"),
Self::UnfinishedTargets { remaining } => {
write!(formatter, "{remaining} targets remain unverified")
}
}
}
}
impl std::error::Error for CommandError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Wire(error) => Some(error),
Self::VerificationFailed { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}