use std::fmt;
use std::sync::Mutex;
use bacnet_encoding::apdu::Apdu;
use bacnet_encoding::npdu::NpduAddress;
use bacnet_types::MacAddr;
use bacnet_types::enums::ConfirmedServiceChoice;
const INVOKE_ID_COUNT: usize = 256;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum CanonicalPeer {
Direct(MacAddr),
Routed {
network: u16,
address: MacAddr,
},
}
impl CanonicalPeer {
pub fn direct(mac: &[u8]) -> Self {
Self::Direct(MacAddr::from_slice(mac))
}
pub fn routed(network: u16, address: &[u8]) -> Self {
Self::Routed {
network,
address: MacAddr::from_slice(address),
}
}
pub fn from_source(
immediate_mac: &[u8],
routed_source: Option<&NpduAddress>,
local_network: Option<u16>,
) -> Self {
match routed_source {
Some(source) if source.mac_address.is_empty() => Self::direct(immediate_mac),
Some(source) if Some(source.network) == local_network => {
Self::direct(&source.mac_address)
}
Some(source) => Self::routed(source.network, &source.mac_address),
None => Self::direct(immediate_mac),
}
}
pub fn routed_alias(
routed_source: Option<&NpduAddress>,
local_network: Option<u16>,
) -> Option<Self> {
routed_source
.filter(|source| {
!source.mac_address.is_empty() && Some(source.network) == local_network
})
.map(|source| Self::routed(source.network, &source.mac_address))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LeaseOwner {
Requester,
Notification,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TerminalPolicy {
SimpleAck,
ComplexAck,
EitherAck,
}
impl TerminalPolicy {
fn accepts_simple_ack(self) -> bool {
matches!(self, Self::SimpleAck | Self::EitherAck)
}
fn accepts_complex_ack(self) -> bool {
matches!(self, Self::ComplexAck | Self::EitherAck)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LeaseMetadata {
owner: LeaseOwner,
peer: CanonicalPeer,
service_choice: ConfirmedServiceChoice,
terminal_policy: TerminalPolicy,
segmented_request: bool,
}
impl LeaseMetadata {
pub fn requester(
peer: CanonicalPeer,
service_choice: ConfirmedServiceChoice,
terminal_policy: TerminalPolicy,
) -> Self {
Self {
owner: LeaseOwner::Requester,
peer,
service_choice,
terminal_policy,
segmented_request: false,
}
}
pub fn segmented_requester(
peer: CanonicalPeer,
service_choice: ConfirmedServiceChoice,
terminal_policy: TerminalPolicy,
) -> Self {
Self {
owner: LeaseOwner::Requester,
peer,
service_choice,
terminal_policy,
segmented_request: true,
}
}
pub fn notification(peer: CanonicalPeer, service_choice: ConfirmedServiceChoice) -> Self {
Self {
owner: LeaseOwner::Notification,
peer,
service_choice,
terminal_policy: TerminalPolicy::SimpleAck,
segmented_request: false,
}
}
pub fn server_read(peer: CanonicalPeer, service_choice: ConfirmedServiceChoice) -> Self {
Self {
owner: LeaseOwner::Notification,
peer,
service_choice,
terminal_policy: TerminalPolicy::ComplexAck,
segmented_request: false,
}
}
pub fn owner(&self) -> LeaseOwner {
self.owner
}
pub fn peer(&self) -> &CanonicalPeer {
&self.peer
}
pub fn service_choice(&self) -> ConfirmedServiceChoice {
self.service_choice
}
pub fn terminal_policy(&self) -> TerminalPolicy {
self.terminal_policy
}
pub fn is_segmented_request(&self) -> bool {
self.segmented_request
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct LeaseToken {
invoke_id: u8,
generation: u64,
}
impl LeaseToken {
pub fn invoke_id(self) -> u8 {
self.invoke_id
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReserveError {
Exhausted,
GenerationExhausted,
StatePoisoned,
}
impl fmt::Display for ReserveError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Exhausted => formatter.write_str("all invoke IDs are leased"),
Self::GenerationExhausted => formatter.write_str("lease generation exhausted"),
Self::StatePoisoned => formatter.write_str("coordinator state is poisoned"),
}
}
}
impl std::error::Error for ReserveError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CoordinatorError {
StatePoisoned,
}
impl fmt::Display for CoordinatorError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("coordinator state is poisoned")
}
}
impl std::error::Error for CoordinatorError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReleaseOutcome {
Released,
AlreadyReleased,
StaleToken,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AdmissionKind {
Terminal,
NonTerminal,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Admission {
token: LeaseToken,
metadata: LeaseMetadata,
kind: AdmissionKind,
}
impl Admission {
pub fn token(&self) -> LeaseToken {
self.token
}
pub fn metadata(&self) -> &LeaseMetadata {
&self.metadata
}
pub fn kind(&self) -> AdmissionKind {
self.kind
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AdmissionOutcome {
Admitted(Admission),
NotOutbound,
UnknownInvokeId,
PeerMismatch,
OwnerMismatch,
ServiceMismatch {
expected: ConfirmedServiceChoice,
observed: ConfirmedServiceChoice,
},
PolicyMismatch,
DirectionMismatch,
DuplicateTerminal,
}
#[derive(Clone, Debug)]
struct ActiveLease {
generation: u64,
metadata: LeaseMetadata,
terminal_claimed: bool,
}
struct CoordinatorState {
slots: Box<[Option<ActiveLease>]>,
last_released: Box<[Option<u64>]>,
next_invoke_id: usize,
last_generation: u64,
active_count: usize,
}
impl CoordinatorState {
fn new() -> Self {
Self {
slots: vec![None; INVOKE_ID_COUNT].into_boxed_slice(),
last_released: vec![None; INVOKE_ID_COUNT].into_boxed_slice(),
next_invoke_id: 0,
last_generation: 0,
active_count: 0,
}
}
fn available_slot(&self) -> Option<usize> {
(0..INVOKE_ID_COUNT)
.map(|offset| (self.next_invoke_id + offset) % INVOKE_ID_COUNT)
.find(|index| self.slots[*index].is_none())
}
fn release_exact(&mut self, token: LeaseToken) -> ReleaseOutcome {
let index = usize::from(token.invoke_id);
match self.slots[index].as_ref() {
Some(active) if active.generation == token.generation => {
self.slots[index] = None;
self.last_released[index] = Some(token.generation);
self.active_count -= 1;
ReleaseOutcome::Released
}
Some(_) => ReleaseOutcome::StaleToken,
None if self.last_released[index] == Some(token.generation) => {
ReleaseOutcome::AlreadyReleased
}
None => ReleaseOutcome::StaleToken,
}
}
}
pub struct OutboundTransactionCoordinator {
state: Mutex<CoordinatorState>,
released: tokio::sync::Notify,
}
impl Default for OutboundTransactionCoordinator {
fn default() -> Self {
Self::new()
}
}
impl OutboundTransactionCoordinator {
pub fn new() -> Self {
Self {
state: Mutex::new(CoordinatorState::new()),
released: tokio::sync::Notify::new(),
}
}
pub fn reserve(&self, metadata: LeaseMetadata) -> Result<LeaseToken, ReserveError> {
let mut state = self.state.lock().map_err(|_| ReserveError::StatePoisoned)?;
let index = state.available_slot().ok_or(ReserveError::Exhausted)?;
let generation = state
.last_generation
.checked_add(1)
.ok_or(ReserveError::GenerationExhausted)?;
let token = LeaseToken {
invoke_id: index as u8,
generation,
};
state.last_generation = generation;
state.next_invoke_id = (index + 1) % INVOKE_ID_COUNT;
state.slots[index] = Some(ActiveLease {
generation,
metadata,
terminal_claimed: false,
});
state.active_count += 1;
Ok(token)
}
pub fn admit(
&self,
peer: &CanonicalPeer,
apdu: &Apdu,
) -> Result<AdmissionOutcome, CoordinatorError> {
let Some(invoke_id) = response_invoke_id(apdu) else {
return Ok(AdmissionOutcome::NotOutbound);
};
let mut state = self
.state
.lock()
.map_err(|_| CoordinatorError::StatePoisoned)?;
let Some(active) = state.slots[usize::from(invoke_id)].as_mut() else {
return Ok(AdmissionOutcome::UnknownInvokeId);
};
if active.metadata.peer != *peer {
return Ok(AdmissionOutcome::PeerMismatch);
}
let kind = match validate_apdu(&active.metadata, apdu) {
Ok(kind) => kind,
Err(outcome) => return Ok(outcome),
};
if kind == AdmissionKind::Terminal && active.terminal_claimed {
return Ok(AdmissionOutcome::DuplicateTerminal);
}
if kind == AdmissionKind::Terminal {
active.terminal_claimed = true;
}
Ok(AdmissionOutcome::Admitted(Admission {
token: LeaseToken {
invoke_id,
generation: active.generation,
},
metadata: active.metadata.clone(),
kind,
}))
}
pub fn admit_from_source(
&self,
immediate_mac: &[u8],
routed_source: Option<&NpduAddress>,
local_network: Option<u16>,
apdu: &Apdu,
) -> Result<AdmissionOutcome, CoordinatorError> {
let peer = CanonicalPeer::from_source(immediate_mac, routed_source, local_network);
match self.admit(&peer, apdu)? {
AdmissionOutcome::PeerMismatch => {
match CanonicalPeer::routed_alias(routed_source, local_network) {
Some(alias) => self.admit(&alias, apdu),
None => Ok(AdmissionOutcome::PeerMismatch),
}
}
outcome => Ok(outcome),
}
}
pub fn complete(&self, token: LeaseToken) -> Result<ReleaseOutcome, CoordinatorError> {
self.release_token(token)
}
pub fn cancel(&self, token: LeaseToken) -> Result<ReleaseOutcome, CoordinatorError> {
self.release_token(token)
}
pub fn release(&self, token: LeaseToken) -> Result<ReleaseOutcome, CoordinatorError> {
self.release_token(token)
}
pub fn active_count(&self) -> Result<usize, CoordinatorError> {
self.state
.lock()
.map(|state| state.active_count)
.map_err(|_| CoordinatorError::StatePoisoned)
}
fn release_token(&self, token: LeaseToken) -> Result<ReleaseOutcome, CoordinatorError> {
let outcome = self
.state
.lock()
.map(|mut state| state.release_exact(token))
.map_err(|_| CoordinatorError::StatePoisoned)?;
if outcome == ReleaseOutcome::Released {
self.released.notify_waiters();
}
Ok(outcome)
}
#[doc(hidden)]
pub fn released(&self) -> tokio::sync::futures::Notified<'_> {
self.released.notified()
}
}
pub fn response_invoke_id(apdu: &Apdu) -> Option<u8> {
match apdu {
Apdu::SimpleAck(pdu) => Some(pdu.invoke_id),
Apdu::ComplexAck(pdu) => Some(pdu.invoke_id),
Apdu::SegmentAck(pdu) => Some(pdu.invoke_id),
Apdu::Error(pdu) => Some(pdu.invoke_id),
Apdu::Reject(pdu) => Some(pdu.invoke_id),
Apdu::Abort(pdu) => Some(pdu.invoke_id),
Apdu::ConfirmedRequest(_) | Apdu::UnconfirmedRequest(_) => None,
}
}
fn validate_apdu(metadata: &LeaseMetadata, apdu: &Apdu) -> Result<AdmissionKind, AdmissionOutcome> {
match apdu {
Apdu::SimpleAck(pdu) => {
validate_service(metadata, pdu.service_choice)?;
if !metadata.terminal_policy.accepts_simple_ack() {
return Err(AdmissionOutcome::PolicyMismatch);
}
Ok(AdmissionKind::Terminal)
}
Apdu::ComplexAck(pdu) => {
if !pdu.segmented {
validate_service(metadata, pdu.service_choice)?;
}
let server_read = !pdu.segmented && metadata.terminal_policy.accepts_complex_ack();
if metadata.owner != LeaseOwner::Requester && !server_read {
return Err(AdmissionOutcome::OwnerMismatch);
}
if !metadata.terminal_policy.accepts_complex_ack() {
return Err(AdmissionOutcome::PolicyMismatch);
}
if pdu.segmented {
Ok(AdmissionKind::NonTerminal)
} else {
Ok(AdmissionKind::Terminal)
}
}
Apdu::Error(pdu) => {
validate_service(metadata, pdu.service_choice)?;
Ok(AdmissionKind::Terminal)
}
Apdu::Reject(_) => Ok(AdmissionKind::Terminal),
Apdu::Abort(pdu) => {
if !pdu.sent_by_server {
return Err(AdmissionOutcome::DirectionMismatch);
}
Ok(AdmissionKind::Terminal)
}
Apdu::SegmentAck(pdu) => {
if !pdu.sent_by_server {
return Err(AdmissionOutcome::DirectionMismatch);
}
if metadata.owner != LeaseOwner::Requester || !metadata.segmented_request {
return Err(AdmissionOutcome::OwnerMismatch);
}
Ok(AdmissionKind::NonTerminal)
}
Apdu::ConfirmedRequest(_) | Apdu::UnconfirmedRequest(_) => {
Err(AdmissionOutcome::NotOutbound)
}
}
}
fn validate_service(
metadata: &LeaseMetadata,
observed: ConfirmedServiceChoice,
) -> Result<(), AdmissionOutcome> {
if metadata.service_choice == observed {
Ok(())
} else {
Err(AdmissionOutcome::ServiceMismatch {
expected: metadata.service_choice,
observed,
})
}
}
#[cfg(test)]
#[path = "coordinator_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "coordinator_peer_tests.rs"]
mod peer_tests;