use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use bytes::Bytes;
use crate::error::{Error, Result};
use crate::message_size::{MessageSize, UDP_RECEIVE_LIMITS};
use crate::v3::UsmSecurityParams;
pub const TIME_WINDOW: u32 = 150;
pub const MAX_ENGINE_TIME: u32 = 2_147_483_647;
#[must_use]
pub fn compute_engine_boots_time(boots_base: u32, total_elapsed_secs: u64) -> (u32, u32) {
let cycle = u64::from(MAX_ENGINE_TIME) + 1;
let additional_boots = total_elapsed_secs / cycle;
let current_time = (total_elapsed_secs % cycle) as u32;
let boots = (u64::from(boots_base) + additional_boots).min(u64::from(MAX_ENGINE_TIME)) as u32;
(boots, current_time)
}
pub const MIN_ENGINE_ID_LEN: usize = 5;
pub const MAX_ENGINE_ID_LEN: usize = 32;
const GENERATED_ENGINE_ID_LEN: usize = 17;
pub fn generate_engine_id() -> Result<Bytes> {
generate_engine_id_with(getrandom::fill)
}
fn generate_engine_id_with(
mut fill: impl FnMut(&mut [u8]) -> std::result::Result<(), getrandom::Error>,
) -> Result<Bytes> {
let mut id = [0_u8; GENERATED_ENGINE_ID_LEN];
fill(&mut id).map_err(|source| Error::RandomSource { source }.boxed())?;
if id.iter().all(|&byte| byte == 0) {
id[GENERATED_ENGINE_ID_LEN - 1] = 1;
} else if id.iter().all(|&byte| byte == 0xff) {
id[GENERATED_ENGINE_ID_LEN - 1] = 0xfe;
}
Ok(Bytes::copy_from_slice(&id))
}
pub fn validate_engine_id(engine_id: &[u8]) -> Result<()> {
let len = engine_id.len();
if !(MIN_ENGINE_ID_LEN..=MAX_ENGINE_ID_LEN).contains(&len) {
return Err(Error::Config(
format!(
"engine ID length {len} out of range (must be {MIN_ENGINE_ID_LEN}..={MAX_ENGINE_ID_LEN} octets)"
)
.into(),
)
.boxed());
}
if engine_id.iter().all(|&b| b == 0x00) {
return Err(Error::Config("engine ID must not be all zero".into()).boxed());
}
if engine_id.iter().all(|&b| b == 0xff) {
return Err(Error::Config("engine ID must not be all 0xff".into()).boxed());
}
Ok(())
}
pub mod report_oids {
use crate::Oid;
use crate::oid;
#[must_use]
pub fn unsupported_sec_levels() -> Oid {
oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 1, 0)
}
#[must_use]
pub fn not_in_time_windows() -> Oid {
oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 2, 0)
}
#[must_use]
pub fn unknown_user_names() -> Oid {
oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 3, 0)
}
#[must_use]
pub fn unknown_engine_ids() -> Oid {
oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 4, 0)
}
#[must_use]
pub fn wrong_digests() -> Oid {
oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 5, 0)
}
#[must_use]
pub fn decryption_errors() -> Oid {
oid!(1, 3, 6, 1, 6, 3, 15, 1, 1, 6, 0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiscoveredEngine {
engine_id: Bytes,
msg_max_size: MessageSize,
}
impl DiscoveredEngine {
pub fn new(engine_id: impl Into<Bytes>, msg_max_size: MessageSize) -> Result<Self> {
let engine_id = engine_id.into();
validate_engine_id(&engine_id)?;
Ok(Self {
engine_id,
msg_max_size,
})
}
#[must_use]
pub fn engine_id(&self) -> &Bytes {
&self.engine_id
}
#[must_use]
pub fn msg_max_size(&self) -> MessageSize {
self.msg_max_size
}
}
#[derive(Debug, Clone)]
pub struct AuthenticatedEngineTime {
boots: u32,
received_time_base: u32,
received_at: Instant,
latest_received_time: u32,
}
impl AuthenticatedEngineTime {
fn new_at(boots: u32, time: u32, now: Instant) -> Self {
Self {
boots,
received_time_base: time,
received_at: now,
latest_received_time: time,
}
}
#[must_use]
pub fn boots(&self) -> u32 {
self.boots
}
#[must_use]
pub fn received_time_base(&self) -> u32 {
self.received_time_base
}
#[must_use]
pub fn latest_received_time(&self) -> u32 {
self.latest_received_time
}
fn estimated_at(&self, now: Instant) -> (u32, u32) {
if self.boots == MAX_ENGINE_TIME {
return (
MAX_ENGINE_TIME,
self.received_time_base.min(MAX_ENGINE_TIME),
);
}
let elapsed = now
.checked_duration_since(self.received_at)
.unwrap_or_default()
.as_secs();
let total_time = u64::from(self.received_time_base).saturating_add(elapsed);
let cycle = u64::from(MAX_ENGINE_TIME) + 1;
let additional_boots = total_time / cycle;
let engine_time = (total_time % cycle) as u32;
let engine_boots =
(u64::from(self.boots) + additional_boots).min(u64::from(MAX_ENGINE_TIME)) as u32;
(engine_boots, engine_time)
}
fn roll_forward_at(&mut self, now: Instant) {
let (estimated_boots, estimated_time) = self.estimated_at(now);
if estimated_boots > self.boots {
self.boots = estimated_boots;
self.received_time_base = estimated_time;
self.received_at = now;
self.latest_received_time = estimated_time;
}
}
fn update_at(&mut self, response_boots: u32, response_time: u32, now: Instant) -> bool {
self.roll_forward_at(now);
if response_boots > self.boots
|| (response_boots == self.boots && response_time > self.latest_received_time)
{
self.boots = response_boots;
self.received_time_base = response_time;
self.received_at = now;
self.latest_received_time = response_time;
true
} else {
false
}
}
}
#[derive(Debug, Clone)]
pub struct EngineState {
discovered: DiscoveredEngine,
authenticated_time: Option<AuthenticatedEngineTime>,
}
impl EngineState {
pub(crate) fn new(engine_id: Bytes, engine_boots: u32, engine_time: u32) -> Self {
Self::with_msg_max_size(
engine_id,
engine_boots,
engine_time,
UDP_RECEIVE_LIMITS.advertised(),
)
}
#[must_use]
pub(crate) fn from_discovery(engine_id: Bytes, msg_max_size: MessageSize) -> Self {
Self {
discovered: DiscoveredEngine {
engine_id,
msg_max_size,
},
authenticated_time: None,
}
}
pub(crate) fn with_msg_max_size(
engine_id: Bytes,
engine_boots: u32,
engine_time: u32,
msg_max_size: MessageSize,
) -> Self {
Self {
discovered: DiscoveredEngine {
engine_id,
msg_max_size,
},
authenticated_time: Some(AuthenticatedEngineTime::new_at(
engine_boots,
engine_time,
Instant::now(),
)),
}
}
#[cfg(test)]
fn with_msg_max_size_capped(
engine_id: Bytes,
engine_boots: u32,
engine_time: u32,
reported_msg_max_size: MessageSize,
session_max: MessageSize,
) -> Self {
Self::with_msg_max_size(
engine_id,
engine_boots,
engine_time,
cap_msg_max_size(reported_msg_max_size, session_max),
)
}
#[must_use]
pub fn engine_id(&self) -> &Bytes {
self.discovered.engine_id()
}
#[must_use]
pub fn discovered(&self) -> &DiscoveredEngine {
&self.discovered
}
#[must_use]
pub fn msg_max_size(&self) -> MessageSize {
self.discovered.msg_max_size()
}
#[must_use]
pub fn authenticated_time(&self) -> Option<&AuthenticatedEngineTime> {
self.authenticated_time.as_ref()
}
#[must_use]
pub(crate) fn estimated_boots_time(&self) -> (u32, u32) {
self.estimated_boots_time_at(Instant::now())
}
pub(crate) fn estimated_boots_time_at(&self, now: Instant) -> (u32, u32) {
self.authenticated_time
.as_ref()
.map_or((0, 0), |time| time.estimated_at(now))
}
pub(crate) fn last_authenticated_update_at(&self) -> Option<Instant> {
self.authenticated_time
.as_ref()
.map(|time| time.received_at)
}
#[cfg(test)]
fn update_time(&mut self, response_boots: u32, response_time: u32) -> bool {
self.update_time_at(response_boots, response_time, Instant::now())
}
fn update_time_at(&mut self, response_boots: u32, response_time: u32, now: Instant) -> bool {
match self.authenticated_time.as_mut() {
Some(time) => time.update_at(response_boots, response_time, now),
None => {
self.authenticated_time = Some(AuthenticatedEngineTime::new_at(
response_boots,
response_time,
now,
));
true
}
}
}
pub(crate) fn merge_from(&mut self, other: &Self) -> bool {
if self.discovered.engine_id != other.discovered.engine_id {
return false;
}
self.discovered.msg_max_size = self
.discovered
.msg_max_size
.min(other.discovered.msg_max_size);
let Some(other_time) = &other.authenticated_time else {
return false;
};
match self.authenticated_time.as_mut() {
Some(time) => time.update_at(
other_time.boots,
other_time.latest_received_time,
other_time.received_at,
),
None => {
self.authenticated_time = Some(other_time.clone());
true
}
}
}
pub(crate) fn check_and_update_timeliness(&mut self, msg_boots: u32, msg_time: u32) -> bool {
self.check_and_update_timeliness_at(msg_boots, msg_time, Instant::now())
}
fn check_and_update_timeliness_at(
&mut self,
msg_boots: u32,
msg_time: u32,
now: Instant,
) -> bool {
self.update_time_at(msg_boots, msg_time, now);
let (local_boots, local_time) = self.estimated_boots_time_at(now);
local_boots != MAX_ENGINE_TIME
&& msg_boots >= local_boots
&& (msg_boots != local_boots || msg_time >= local_time.saturating_sub(TIME_WINDOW))
}
#[must_use]
#[cfg(test)]
fn is_in_time_window(&self, msg_boots: u32, msg_time: u32) -> bool {
let (local_boots, local_time) = self.estimated_boots_time();
in_authoritative_time_window(local_boots, local_time, msg_boots, msg_time)
}
}
fn cap_msg_max_size(reported: MessageSize, session_max: MessageSize) -> MessageSize {
if reported > session_max {
tracing::debug!(target: "async_snmp::v3", { reported = reported.as_usize(), session_max = session_max.as_usize() }, "capping msgMaxSize to session limit");
session_max
} else {
reported
}
}
fn validate_authenticated_time(engine_boots: u32, engine_time: u32) -> Result<()> {
if engine_boots > MAX_ENGINE_TIME {
return Err(Error::Config(
format!(
"authenticated engine boots {engine_boots} out of range (must be 0..={MAX_ENGINE_TIME})"
)
.into(),
)
.boxed());
}
if engine_time > MAX_ENGINE_TIME {
return Err(Error::Config(
format!(
"authenticated engine time {engine_time} out of range (must be 0..={MAX_ENGINE_TIME})"
)
.into(),
)
.boxed());
}
Ok(())
}
pub fn in_authoritative_time_window(
local_boots: u32,
local_time: u32,
msg_boots: u32,
msg_time: u32,
) -> bool {
local_boots != MAX_ENGINE_TIME
&& msg_boots == local_boots
&& msg_time.abs_diff(local_time) <= TIME_WINDOW
}
const DEFAULT_ENGINE_CACHE_TTL: Duration = Duration::from_secs(300);
#[derive(Debug)]
struct CachedTarget {
engine_id: Bytes,
msg_max_size: MessageSize,
refreshed_at: Instant,
}
#[derive(Debug, Default)]
struct EngineCacheInner {
targets: HashMap<SocketAddr, CachedTarget>,
authenticated_times: HashMap<Bytes, AuthenticatedEngineTime>,
}
#[derive(Debug)]
enum StoreOutcome {
Stored(EngineState),
IdentityConflict(EngineState),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TimelinessCandidateOutcome {
Timely,
MissingMapping,
Stale,
IdentityConflict,
}
#[derive(Debug)]
pub(crate) enum TimelinessPublicationOutcome {
Published(EngineState),
RestoredMapping(EngineState),
Stale(EngineState),
IdentityConflict,
}
#[derive(Debug)]
pub struct EngineCache {
inner: RwLock<EngineCacheInner>,
pub(crate) discovery_coordinator: std::sync::Arc<crate::client::DiscoveryCoordinator>,
recoveries: AtomicU64,
max_capacity: Option<usize>,
ttl: Duration,
#[cfg(test)]
panic_stage: std::sync::atomic::AtomicU8,
}
impl Default for EngineCache {
fn default() -> Self {
Self::new()
}
}
impl EngineCache {
#[must_use]
pub fn new() -> Self {
Self {
inner: RwLock::new(EngineCacheInner::default()),
discovery_coordinator: std::sync::Arc::new(crate::client::DiscoveryCoordinator::new()),
recoveries: AtomicU64::new(0),
max_capacity: None,
ttl: DEFAULT_ENGINE_CACHE_TTL,
#[cfg(test)]
panic_stage: std::sync::atomic::AtomicU8::new(0),
}
}
fn record_recovery(&self) {
let _ = self
.recoveries
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
Some(current.saturating_add(1))
});
}
fn clear_inner(inner: &mut EngineCacheInner) {
inner.targets.clear();
inner.authenticated_times.clear();
}
fn with_inner<R>(&self, mutation: impl FnOnce(&mut EngineCacheInner) -> R) -> R {
let mut inner = match self.inner.write() {
Ok(inner) => inner,
Err(poisoned) => {
let mut inner = poisoned.into_inner();
Self::clear_inner(&mut inner);
self.record_recovery();
self.inner.clear_poison();
inner
}
};
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| mutation(&mut inner))) {
Ok(result) => result,
Err(payload) => {
Self::clear_inner(&mut inner);
self.record_recovery();
drop(inner);
std::panic::resume_unwind(payload);
}
}
}
#[must_use]
pub fn recovery_count(&self) -> u64 {
self.recoveries.load(Ordering::Relaxed)
}
#[cfg(test)]
fn mutation_checkpoint(&self, stage: CacheMutationStage) {
if self
.panic_stage
.compare_exchange(stage as u8, 0, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
panic!("injected engine cache mutation panic at {stage:?}");
}
}
#[must_use]
pub fn with_max_capacity(mut self, max_capacity: usize) -> Self {
self.max_capacity = Some(max_capacity.max(1));
self
}
#[must_use]
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
pub fn get(&self, target: &SocketAddr) -> Option<EngineState> {
self.get_at(target, Instant::now())
}
fn get_at(&self, target: &SocketAddr, now: Instant) -> Option<EngineState> {
self.with_inner(|inner| {
self.expire_target_if_needed(inner, target, now);
compose_cached_state(inner, target)
})
}
pub fn insert_discovered(&self, target: SocketAddr, engine: DiscoveredEngine) -> Result<()> {
let expected_engine_id = engine.engine_id.clone();
let state = EngineState::from_discovery(engine.engine_id, engine.msg_max_size);
let outcome = self.store_at(target, state, Instant::now(), false);
match outcome {
StoreOutcome::Stored(stored) if stored.engine_id() == &expected_engine_id => Ok(()),
StoreOutcome::IdentityConflict(_) | StoreOutcome::Stored(_) => Err(Error::Config(
"target is already mapped to a different unexpired engine identity".into(),
)
.boxed()),
}
}
pub(crate) fn insert_state(&self, target: SocketAddr, state: EngineState) -> EngineState {
self.insert_state_at(target, state, Instant::now())
}
fn insert_state_at(&self, target: SocketAddr, state: EngineState, now: Instant) -> EngineState {
match self.store_at(target, state, now, false) {
StoreOutcome::Stored(state) | StoreOutcome::IdentityConflict(state) => state,
}
}
pub fn seed_authenticated(
&self,
target: SocketAddr,
engine: DiscoveredEngine,
engine_boots: u32,
engine_time: u32,
) -> Result<()> {
validate_authenticated_time(engine_boots, engine_time)?;
let expected_engine_id = engine.engine_id.clone();
let state = EngineState::with_msg_max_size(
engine.engine_id,
engine_boots,
engine_time,
engine.msg_max_size,
);
let outcome = self.store_at(target, state, Instant::now(), false);
match outcome {
StoreOutcome::Stored(stored) if stored.engine_id() == &expected_engine_id => Ok(()),
StoreOutcome::IdentityConflict(_) | StoreOutcome::Stored(_) => Err(Error::Config(
"target is already mapped to a different unexpired engine identity".into(),
)
.boxed()),
}
}
pub(crate) fn replace_target(&self, target: SocketAddr, state: EngineState) -> EngineState {
match self.store_at(target, state, Instant::now(), true) {
StoreOutcome::Stored(state) => state,
StoreOutcome::IdentityConflict(_) => {
unreachable!("explicit replacement cannot report an identity conflict")
}
}
}
fn store_at(
&self,
target: SocketAddr,
state: EngineState,
now: Instant,
replace_identity: bool,
) -> StoreOutcome {
self.with_inner(|inner| {
self.expire_target_if_needed(inner, &target, now);
if !replace_identity
&& let Some(existing) = inner.targets.get(&target)
&& existing.engine_id != *state.engine_id()
{
return StoreOutcome::IdentityConflict(
compose_cached_state(inner, &target)
.expect("existing target must compose into cached state"),
);
}
let evicted = if let Some(cap) = self.max_capacity
&& !inner.targets.contains_key(&target)
&& inner.targets.len() >= cap
{
inner
.targets
.iter()
.min_by_key(|(_, cached)| cached.refreshed_at)
.map(|(target, cached)| (*target, cached.engine_id.clone()))
} else {
None
};
let replaced_engine = inner
.targets
.get(&target)
.filter(|cached| cached.engine_id != *state.engine_id())
.map(|cached| cached.engine_id.clone());
if let Some(authenticated) = &state.authenticated_time {
merge_authenticated_time(
&mut inner.authenticated_times,
state.engine_id(),
authenticated,
);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeStored);
}
let engine_id = state.discovered.engine_id;
let msg_max_size = state.discovered.msg_max_size;
inner.targets.insert(
target,
CachedTarget {
engine_id,
msg_max_size,
refreshed_at: now,
},
);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::TargetStored);
if let Some((oldest_target, oldest_engine)) = evicted {
inner.targets.remove(&oldest_target);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::TargetRemoved);
remove_orphaned_time(inner, &oldest_engine);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeRemoved);
}
if let Some(replaced_engine) = replaced_engine {
remove_orphaned_time(inner, &replaced_engine);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeRemoved);
}
StoreOutcome::Stored(
compose_cached_state(inner, &target)
.expect("stored target must compose into cached state"),
)
})
}
#[cfg(test)]
fn update_time(&self, target: &SocketAddr, response_boots: u32, response_time: u32) -> bool {
self.update_time_at(target, response_boots, response_time, Instant::now())
}
#[cfg(test)]
fn update_time_at(
&self,
target: &SocketAddr,
response_boots: u32,
response_time: u32,
now: Instant,
) -> bool {
self.with_inner(|inner| {
let Some(engine_id) = inner
.targets
.get(target)
.map(|cached| cached.engine_id.clone())
else {
return false;
};
let changed = match inner.authenticated_times.get_mut(&engine_id) {
Some(time) => time.update_at(response_boots, response_time, now),
None => {
inner.authenticated_times.insert(
engine_id,
AuthenticatedEngineTime::new_at(response_boots, response_time, now),
);
true
}
};
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeStored);
if let Some(cached) = inner.targets.get_mut(target) {
cached.refreshed_at = now;
}
changed
})
}
pub(crate) fn check_and_update_timeliness(
&self,
target: &SocketAddr,
local_state: &EngineState,
engine_id: &[u8],
msg_boots: u32,
msg_time: u32,
) -> TimelinessPublicationOutcome {
self.check_and_update_timeliness_at(
target,
local_state,
engine_id,
msg_boots,
msg_time,
Instant::now(),
)
}
pub(crate) fn timeliness_candidate(
&self,
target: &SocketAddr,
local_state: &EngineState,
engine_id: &[u8],
msg_boots: u32,
msg_time: u32,
) -> TimelinessCandidateOutcome {
self.timeliness_candidate_at(
target,
local_state,
engine_id,
msg_boots,
msg_time,
Instant::now(),
)
}
fn timeliness_candidate_at(
&self,
target: &SocketAddr,
local_state: &EngineState,
engine_id: &[u8],
msg_boots: u32,
msg_time: u32,
now: Instant,
) -> TimelinessCandidateOutcome {
if local_state.engine_id().as_ref() != engine_id {
return TimelinessCandidateOutcome::IdentityConflict;
}
self.with_inner(|inner| {
self.expire_target_if_needed(inner, target, now);
let Some(cached_engine_id) = inner
.targets
.get(target)
.map(|cached| cached.engine_id.clone())
else {
let mut candidate = local_state.clone();
return if candidate.check_and_update_timeliness_at(msg_boots, msg_time, now) {
TimelinessCandidateOutcome::MissingMapping
} else {
TimelinessCandidateOutcome::Stale
};
};
if cached_engine_id.as_ref() != engine_id {
return TimelinessCandidateOutcome::IdentityConflict;
}
let mut candidate = local_state.clone();
candidate.merge_from(
&compose_cached_state(inner, target)
.expect("existing target must compose into cached state"),
);
if candidate.check_and_update_timeliness_at(msg_boots, msg_time, now) {
TimelinessCandidateOutcome::Timely
} else {
TimelinessCandidateOutcome::Stale
}
})
}
fn check_and_update_timeliness_at(
&self,
target: &SocketAddr,
local_state: &EngineState,
engine_id: &[u8],
msg_boots: u32,
msg_time: u32,
now: Instant,
) -> TimelinessPublicationOutcome {
if local_state.engine_id().as_ref() != engine_id {
return TimelinessPublicationOutcome::IdentityConflict;
}
self.with_inner(|inner| {
self.expire_target_if_needed(inner, target, now);
let mapping_missing = match inner.targets.get(target) {
Some(cached) if cached.engine_id.as_ref() != engine_id => {
return TimelinessPublicationOutcome::IdentityConflict;
}
Some(_) => false,
None => true,
};
let engine_id = local_state.engine_id().clone();
let mut state = local_state.clone();
if let Some(shared_time) = inner.authenticated_times.get(&engine_id) {
state.merge_from(&EngineState {
discovered: state.discovered.clone(),
authenticated_time: Some(shared_time.clone()),
});
}
let timely = state.check_and_update_timeliness_at(msg_boots, msg_time, now);
if let Some(authenticated) = &state.authenticated_time {
merge_authenticated_time(&mut inner.authenticated_times, &engine_id, authenticated);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeStored);
}
if timely {
if mapping_missing {
let evicted = if let Some(cap) = self.max_capacity
&& inner.targets.len() >= cap
{
inner
.targets
.iter()
.min_by_key(|(_, cached)| cached.refreshed_at)
.map(|(target, cached)| (*target, cached.engine_id.clone()))
} else {
None
};
inner.targets.insert(
*target,
CachedTarget {
engine_id: engine_id.clone(),
msg_max_size: local_state.msg_max_size(),
refreshed_at: now,
},
);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::TargetStored);
if let Some((oldest_target, oldest_engine)) = evicted {
inner.targets.remove(&oldest_target);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::TargetRemoved);
remove_orphaned_time(inner, &oldest_engine);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeRemoved);
}
} else {
inner
.targets
.get_mut(target)
.expect("current target mapping must remain present")
.refreshed_at = now;
}
let state = compose_cached_state(inner, target)
.expect("published target must compose into cached state");
return if mapping_missing {
TimelinessPublicationOutcome::RestoredMapping(state)
} else {
TimelinessPublicationOutcome::Published(state)
};
}
if mapping_missing {
remove_orphaned_time(inner, &engine_id);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeRemoved);
} else {
state = compose_cached_state(inner, target)
.expect("existing target must compose into cached state");
}
TimelinessPublicationOutcome::Stale(state)
})
}
pub fn remove(&self, target: &SocketAddr) -> Option<EngineState> {
self.with_inner(|inner| {
let state = compose_cached_state(inner, target)?;
let cached = inner.targets.remove(target)?;
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::TargetRemoved);
remove_orphaned_time(inner, &cached.engine_id);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeRemoved);
Some(state)
})
}
pub fn clear(&self) {
self.with_inner(|inner| {
inner.targets.clear();
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::TargetsCleared);
inner.authenticated_times.clear();
});
}
pub fn len(&self) -> usize {
self.with_inner(|inner| inner.targets.len())
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[cfg(test)]
pub(crate) fn poison_for_test(&self) {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = self.inner.write().expect("engine cache test lock");
panic!("poison engine cache for test");
}));
assert!(self.inner.is_poisoned());
}
#[cfg(test)]
fn inject_panic_at(&self, stage: CacheMutationStage) {
self.panic_stage.store(stage as u8, Ordering::Relaxed);
}
fn expire_target_if_needed(
&self,
inner: &mut EngineCacheInner,
target: &SocketAddr,
now: Instant,
) {
let expired_engine = inner.targets.get(target).and_then(|cached| {
(now.checked_duration_since(cached.refreshed_at)
.unwrap_or_default()
> self.ttl)
.then(|| cached.engine_id.clone())
});
if let Some(engine_id) = expired_engine {
inner.targets.remove(target);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::TargetRemoved);
remove_orphaned_time(inner, &engine_id);
#[cfg(test)]
self.mutation_checkpoint(CacheMutationStage::AuthenticatedTimeRemoved);
}
}
}
#[cfg(test)]
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
enum CacheMutationStage {
AuthenticatedTimeStored = 1,
TargetStored = 2,
TargetRemoved = 3,
AuthenticatedTimeRemoved = 4,
TargetsCleared = 5,
}
fn compose_cached_state(inner: &EngineCacheInner, target: &SocketAddr) -> Option<EngineState> {
let cached = inner.targets.get(target)?;
Some(EngineState {
discovered: DiscoveredEngine {
engine_id: cached.engine_id.clone(),
msg_max_size: cached.msg_max_size,
},
authenticated_time: inner.authenticated_times.get(&cached.engine_id).cloned(),
})
}
fn merge_authenticated_time(
authenticated_times: &mut HashMap<Bytes, AuthenticatedEngineTime>,
engine_id: &Bytes,
incoming: &AuthenticatedEngineTime,
) {
match authenticated_times.get_mut(engine_id) {
Some(current) => {
current.update_at(
incoming.boots,
incoming.latest_received_time,
incoming.received_at,
);
}
None => {
authenticated_times.insert(engine_id.clone(), incoming.clone());
}
}
}
fn remove_orphaned_time(inner: &mut EngineCacheInner, engine_id: &Bytes) {
if !inner
.targets
.values()
.any(|cached| cached.engine_id == engine_id)
{
inner.authenticated_times.remove(engine_id);
}
}
pub fn parse_discovery_response(security_params: &Bytes) -> Result<DiscoveredEngine> {
parse_discovery_response_with_msg_max_size(security_params, UDP_RECEIVE_LIMITS.advertised())
}
pub(crate) fn parse_discovery_response_with_msg_max_size(
security_params: &Bytes,
reported_msg_max_size: MessageSize,
) -> Result<DiscoveredEngine> {
let usm = UsmSecurityParams::decode(security_params.clone(), crate::DecodeConfig::default())?;
discovered_engine(usm.value.engine_id, reported_msg_max_size)
}
pub(crate) fn discovered_engine_state(
engine_id: Bytes,
reported_msg_max_size: MessageSize,
) -> Result<EngineState> {
let engine = discovered_engine(engine_id, reported_msg_max_size)?;
Ok(EngineState::from_discovery(
engine.engine_id,
engine.msg_max_size,
))
}
fn discovered_engine(
engine_id: Bytes,
reported_msg_max_size: MessageSize,
) -> Result<DiscoveredEngine> {
validate_discovered_engine_id(&engine_id)?;
Ok(DiscoveredEngine {
engine_id,
msg_max_size: reported_msg_max_size,
})
}
pub fn parse_discovery_response_with_limits(
security_params: &Bytes,
reported_msg_max_size: MessageSize,
session_max: MessageSize,
) -> Result<DiscoveredEngine> {
let usm = UsmSecurityParams::decode(security_params.clone(), crate::DecodeConfig::default())?;
discovered_engine(
usm.value.engine_id,
cap_msg_max_size(reported_msg_max_size, session_max),
)
}
fn validate_discovered_engine_id(engine_id: &[u8]) -> Result<()> {
if validate_engine_id(engine_id).is_err() {
tracing::debug!(target: "async_snmp::engine", { length = engine_id.len() }, "discovery response contained invalid engine ID");
return Err(Error::InvalidMessage(
"discovery response contains an invalid authoritative engine ID".into(),
)
.boxed());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_engine_id_is_valid() {
let id = generate_engine_id().unwrap();
assert_eq!(id.len(), GENERATED_ENGINE_ID_LEN);
validate_engine_id(&id).expect("generated engine ID must validate");
}
#[test]
fn test_generate_engine_id_uses_random_bytes_as_opaque_identifier() {
let expected = [0x42; GENERATED_ENGINE_ID_LEN];
let id = generate_engine_id_with(|output| {
output.copy_from_slice(&expected);
Ok(())
})
.unwrap();
assert_eq!(id.as_ref(), expected);
}
#[test]
fn test_generate_engine_id_avoids_reserved_values() {
for reserved in [0x00, 0xff] {
let id = generate_engine_id_with(|output| {
output.fill(reserved);
Ok(())
})
.unwrap();
validate_engine_id(&id).expect("generated engine ID must not be reserved");
}
}
#[test]
fn test_generate_engine_id_distinct_across_generations() {
let a = generate_engine_id().unwrap();
let b = generate_engine_id().unwrap();
assert_ne!(a, b, "two generated engine IDs must not collide");
}
#[test]
fn test_generate_engine_id_propagates_random_source_failure() {
let error = generate_engine_id_with(|_| Err(getrandom::Error::UNEXPECTED)).unwrap_err();
assert_eq!(error.kind(), crate::ErrorKind::RandomSource);
}
#[test]
fn test_validate_engine_id_rejects_invalid() {
assert!(validate_engine_id(&[0x80, 0x00, 0x00, 0x01]).is_err());
assert!(validate_engine_id(&[0x11; MAX_ENGINE_ID_LEN + 1]).is_err());
assert!(validate_engine_id(&[0x00; 8]).is_err());
assert!(validate_engine_id(&[0xff; 8]).is_err());
}
#[test]
fn test_validate_engine_id_accepts_valid() {
validate_engine_id(&[0x80, 0x00, 0x00, 0x00, 0x01]).unwrap();
validate_engine_id(&[0x22; MAX_ENGINE_ID_LEN]).unwrap();
validate_engine_id(b"my-engine").unwrap();
}
#[test]
fn test_engine_state_estimated_time() {
let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
let estimated = state.estimated_boots_time().1;
assert!(estimated >= 1000);
}
#[test]
fn test_engine_state_update_time() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
assert!(state.update_time(1, 1100));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1100
);
assert!(!state.update_time(1, 1050));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1100
);
assert!(state.update_time(2, 500));
assert_eq!(state.authenticated_time().unwrap().boots(), 2);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
500
);
}
#[test]
fn test_anti_replay_rejects_old_time() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
assert!(state.update_time(1, 1500));
assert!(
!state.update_time(1, 1400),
"Should reject replay: time 1400 < latest 1500"
);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1500,
"Latest should not change"
);
assert!(
!state.update_time(1, 1500),
"Should reject replay: time 1500 == latest 1500"
);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1500
);
assert!(
state.update_time(1, 1501),
"Should accept: time 1501 > latest 1500"
);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1501
);
}
#[test]
fn test_anti_replay_new_boot_cycle_resets() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
assert!(state.update_time(1, 5000));
assert!(
state.update_time(2, 100),
"New boot cycle should accept even with lower time"
);
assert_eq!(state.authenticated_time().unwrap().boots(), 2);
assert_eq!(
state.authenticated_time().unwrap().received_time_base(),
100
);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
100,
"Latest should reset to new time"
);
assert!(
!state.update_time(2, 50),
"Should reject older time in same boot cycle"
);
assert!(state.update_time(2, 150), "Should accept newer time");
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
150
);
}
#[test]
fn test_anti_replay_rejects_old_boot_cycle() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 5, 1000);
assert!(
!state.update_time(4, 9999),
"Should reject old boot cycle even with high time"
);
assert_eq!(
state.authenticated_time().unwrap().boots(),
5,
"Boots should not change"
);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1000,
"Latest should not change"
);
assert!(!state.update_time(0, 9999), "Should reject boots=0 replay");
}
#[test]
fn test_anti_replay_boundary_values() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 0);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
0
);
assert!(state.update_time(1, 1));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1
);
assert!(!state.update_time(1, 0));
assert!(state.update_time(1, MAX_ENGINE_TIME - 1));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
MAX_ENGINE_TIME - 1
);
assert!(state.update_time(1, MAX_ENGINE_TIME));
assert_eq!(state.estimated_boots_time(), (1, MAX_ENGINE_TIME));
assert!(!state.update_time(1, MAX_ENGINE_TIME));
}
#[test]
fn test_engine_state_time_window() {
let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
assert!(state.is_in_time_window(1, 1000));
assert!(state.is_in_time_window(1, 1100)); assert!(state.is_in_time_window(1, 900));
assert!(!state.is_in_time_window(2, 1000));
assert!(!state.is_in_time_window(0, 1000));
assert!(!state.is_in_time_window(1, 2000)); }
#[test]
fn test_time_window_150s_exact_boundary() {
let state = EngineState::new(Bytes::from_static(b"engine"), 1, 10000);
assert!(
state.is_in_time_window(1, 10150),
"Message at exactly +150s boundary should be in window"
);
assert!(
!state.is_in_time_window(1, 10151),
"Message at +151s should be outside window"
);
assert!(
state.is_in_time_window(1, 9850),
"Message at exactly -150s boundary should be in window"
);
assert!(
!state.is_in_time_window(1, 9849),
"Message at -151s should be outside window"
);
}
#[test]
fn test_time_window_boots_latched() {
let state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 1000);
assert!(
!state.is_in_time_window(2_147_483_647, 1000),
"Latched boots should reject all messages"
);
assert!(!state.is_in_time_window(2_147_483_647, 1100));
assert!(!state.is_in_time_window(2_147_483_647, 900));
}
#[test]
fn test_time_window_boots_mismatch() {
let state = EngineState::new(Bytes::from_static(b"engine"), 100, 1000);
assert!(!state.is_in_time_window(101, 1000));
assert!(!state.is_in_time_window(200, 1000));
assert!(!state.is_in_time_window(99, 1000));
assert!(!state.is_in_time_window(0, 1000));
}
#[test]
fn test_check_and_update_timeliness_within_window_accepted() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
assert!(state.check_and_update_timeliness(3, 900));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1000
);
assert!(state.check_and_update_timeliness(3, 850));
}
#[test]
fn test_check_and_update_timeliness_controllable_boundary_without_rollback() {
let now = Instant::now();
let mut at_boundary = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
at_boundary.authenticated_time.as_mut().unwrap().received_at = now;
assert!(at_boundary.check_and_update_timeliness_at(3, 950, now + Duration::from_secs(100)));
assert_eq!(
at_boundary
.authenticated_time()
.unwrap()
.latest_received_time(),
1000,
"an older in-window message must not lower the high-water mark"
);
let mut outside = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
outside.authenticated_time.as_mut().unwrap().received_at = now;
assert!(!outside.check_and_update_timeliness_at(3, 949, now + Duration::from_secs(100)));
assert_eq!(
outside.authenticated_time().unwrap().latest_received_time(),
1000
);
}
#[test]
fn test_check_and_update_timeliness_newer_time_updates_lcd() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
assert!(state.check_and_update_timeliness(3, 1200));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1200
);
assert_eq!(
state.authenticated_time().unwrap().received_time_base(),
1200
);
}
#[test]
fn test_check_and_update_timeliness_stale_time_rejected() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
assert!(!state.check_and_update_timeliness(3, 500));
assert!(!state.check_and_update_timeliness(3, 849));
}
#[test]
fn test_check_and_update_timeliness_old_boots_rejected() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
assert!(!state.check_and_update_timeliness(2, 5000));
assert_eq!(
state.authenticated_time().unwrap().boots(),
3,
"old boot cycle must not update LCD"
);
}
#[test]
fn test_check_and_update_timeliness_reboot_accepted() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 3, 1000);
assert!(state.check_and_update_timeliness(4, 10));
assert_eq!(state.authenticated_time().unwrap().boots(), 4);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
10
);
assert!(!state.check_and_update_timeliness(3, 99999));
}
#[test]
fn test_check_and_update_timeliness_latched_boots_rejected() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), MAX_ENGINE_TIME, 1000);
assert!(!state.check_and_update_timeliness(MAX_ENGINE_TIME, 1000));
}
#[test]
fn test_engine_cache_basic_operations() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
assert!(cache.is_empty());
assert!(cache.get(&addr).is_none());
let state = EngineState::new(Bytes::from_static(b"engine1"), 1, 1000);
cache.insert_state(addr, state);
assert_eq!(cache.len(), 1);
assert!(!cache.is_empty());
let retrieved = cache.get(&addr).unwrap();
assert_eq!(retrieved.engine_id().as_ref(), b"engine1");
assert_eq!(retrieved.authenticated_time().unwrap().boots(), 1);
assert!(cache.update_time(&addr, 1, 1100));
let removed = cache.remove(&addr).unwrap();
assert_eq!(
removed.authenticated_time().unwrap().latest_received_time(),
1100
);
assert!(cache.is_empty());
}
#[test]
fn test_public_discovery_insert_cannot_establish_authenticated_time() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let discovered = DiscoveredEngine::new(
Bytes::from_static(b"remote-engine"),
crate::MessageSize::new(1400).unwrap(),
)
.unwrap();
cache.insert_discovered(addr, discovered).unwrap();
let cached = cache.get(&addr).unwrap();
assert_eq!(cached.engine_id(), b"remote-engine".as_slice());
assert!(cached.authenticated_time().is_none());
assert_eq!(cached.estimated_boots_time(), (0, 0));
}
#[test]
fn test_authenticated_seed_validates_invariants_before_publication() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let discovered = || {
DiscoveredEngine::new(
Bytes::from_static(b"remote-engine"),
crate::MessageSize::new(1400).unwrap(),
)
.unwrap()
};
assert!(
cache
.seed_authenticated(addr, discovered(), MAX_ENGINE_TIME + 1, 10)
.is_err()
);
assert!(
cache
.seed_authenticated(addr, discovered(), 1, MAX_ENGINE_TIME + 1)
.is_err()
);
assert!(cache.is_empty());
cache.seed_authenticated(addr, discovered(), 0, 10).unwrap();
let zero_boots = cache.get(&addr).unwrap();
assert_eq!(zero_boots.authenticated_time().unwrap().boots(), 0);
cache
.seed_authenticated(addr, discovered(), 7, 500)
.unwrap();
let cached = cache.get(&addr).unwrap();
assert_eq!(cached.engine_id(), b"remote-engine".as_slice());
let time = cached.authenticated_time().unwrap();
assert_eq!((time.boots(), time.latest_received_time()), (7, 500));
}
#[test]
fn test_public_cache_inserts_reject_active_identity_conflicts() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let capacity = crate::MessageSize::new(1400).unwrap();
cache
.insert_discovered(
addr,
DiscoveredEngine::new(Bytes::from_static(b"engine-one"), capacity).unwrap(),
)
.unwrap();
let error = cache
.insert_discovered(
addr,
DiscoveredEngine::new(Bytes::from_static(b"engine-two"), capacity).unwrap(),
)
.unwrap_err();
assert!(matches!(*error, Error::Config(_)));
assert_eq!(
cache.get(&addr).unwrap().engine_id(),
b"engine-one".as_slice()
);
}
#[test]
fn public_insert_recovers_a_poisoned_cache() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
cache.insert_state(
addr,
EngineState::new(Bytes::from_static(b"previous-engine"), 1, 10),
);
cache.poison_for_test();
cache
.insert_discovered(
addr,
DiscoveredEngine::new(
Bytes::from_static(b"remote-engine"),
crate::MessageSize::new(1400).unwrap(),
)
.unwrap(),
)
.unwrap();
assert_eq!(cache.recovery_count(), 1);
let recovered = cache.get(&addr).unwrap();
assert_eq!(recovered.engine_id(), b"remote-engine".as_slice());
assert!(recovered.authenticated_time().is_none());
}
#[test]
fn public_operations_repair_poison_before_reporting_state() {
for operation in 0..4 {
let cache = EngineCache::new();
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
cache.insert_state(addr, EngineState::new(Bytes::from_static(b"engine"), 1, 10));
cache.poison_for_test();
match operation {
0 => assert!(cache.get(&addr).is_none()),
1 => assert_eq!(cache.len(), 0),
2 => assert!(cache.remove(&addr).is_none()),
3 => cache.clear(),
_ => unreachable!(),
}
assert_eq!(cache.recovery_count(), 1);
assert!(!cache.inner.is_poisoned());
let inner = cache.inner.read().unwrap();
assert!(inner.targets.is_empty());
assert!(inner.authenticated_times.is_empty());
}
}
#[test]
fn mutation_panics_clear_coupled_state_and_preserve_cache_policies() {
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let other: SocketAddr = "192.0.2.2:161".parse().unwrap();
for stage in [
CacheMutationStage::AuthenticatedTimeStored,
CacheMutationStage::TargetStored,
CacheMutationStage::TargetRemoved,
CacheMutationStage::AuthenticatedTimeRemoved,
CacheMutationStage::TargetsCleared,
] {
let cache = EngineCache::new()
.with_max_capacity(1)
.with_ttl(Duration::from_secs(5));
let now = Instant::now();
if matches!(
stage,
CacheMutationStage::TargetRemoved
| CacheMutationStage::AuthenticatedTimeRemoved
| CacheMutationStage::TargetsCleared
) {
cache.insert_state_at(
addr,
EngineState::new(Bytes::from_static(b"seed-engine"), 1, 10),
now,
);
}
cache.inject_panic_at(stage);
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| match stage {
CacheMutationStage::AuthenticatedTimeStored => {
cache.insert_state_at(
addr,
EngineState::new(Bytes::from_static(b"engine"), 1, 10),
now,
);
}
CacheMutationStage::TargetStored => {
cache.insert_state_at(
addr,
EngineState::from_discovery(
Bytes::from_static(b"engine"),
crate::MessageSize::new(1400).unwrap(),
),
now,
);
}
CacheMutationStage::TargetRemoved
| CacheMutationStage::AuthenticatedTimeRemoved => {
let _ = cache.remove(&addr);
}
CacheMutationStage::TargetsCleared => cache.clear(),
}));
assert!(panic.is_err(), "stage {stage:?} must preserve the panic");
assert_eq!(cache.recovery_count(), 1, "stage {stage:?}");
assert!(!cache.inner.is_poisoned(), "stage {stage:?}");
{
let inner = cache.inner.read().unwrap();
assert!(inner.targets.is_empty(), "stage {stage:?}");
assert!(inner.authenticated_times.is_empty(), "stage {stage:?}");
}
cache.insert_state_at(
addr,
EngineState::new(Bytes::from_static(b"first"), 1, 20),
now,
);
cache.insert_state_at(
other,
EngineState::new(Bytes::from_static(b"second"), 1, 30),
now + Duration::from_secs(1),
);
assert!(cache.get_at(&addr, now + Duration::from_secs(1)).is_none());
assert!(cache.get_at(&other, now + Duration::from_secs(5)).is_some());
assert!(cache.get_at(&other, now + Duration::from_secs(7)).is_none());
let inner = cache.inner.read().unwrap();
assert!(inner.authenticated_times.is_empty(), "stage {stage:?}");
}
}
#[test]
fn test_engine_cache_explicit_replacement_latches_new_identity() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let shared_addr: SocketAddr = "192.168.1.2:161".parse().unwrap();
let old = EngineState::from_discovery(
Bytes::from_static(b"old-engine"),
crate::MessageSize::new(1400).unwrap(),
);
let new = EngineState::from_discovery(
Bytes::from_static(b"new-engine"),
crate::MessageSize::new(1500).unwrap(),
);
let shared = EngineState::new(Bytes::from_static(b"new-engine"), 7, 500);
cache.insert_state(addr, old.clone());
cache.insert_state(shared_addr, shared);
let replaced = cache.replace_target(addr, new);
cache.insert_state(addr, old);
assert_eq!(replaced.engine_id().as_ref(), b"new-engine");
let trusted = replaced.authenticated_time().unwrap();
assert_eq!((trusted.boots(), trusted.latest_received_time()), (7, 500));
let cached = cache.get(&addr).unwrap();
assert_eq!(cached.engine_id().as_ref(), b"new-engine");
assert_eq!(cached.msg_max_size(), 1500);
}
#[test]
fn test_engine_cache_shares_authenticated_time_by_engine_id() {
let cache = EngineCache::new();
let addr1: SocketAddr = "192.168.1.1:161".parse().unwrap();
let addr2: SocketAddr = "192.168.1.2:161".parse().unwrap();
let engine_id = Bytes::from_static(b"shared-engine");
cache.insert_state(
addr1,
EngineState::from_discovery(engine_id.clone(), crate::MessageSize::new(1400).unwrap()),
);
cache.insert_state(
addr2,
EngineState::from_discovery(engine_id, crate::MessageSize::new(1500).unwrap()),
);
assert!(cache.update_time(&addr1, 4, 500));
let state2 = cache.get(&addr2).unwrap();
let trusted = state2.authenticated_time().unwrap();
assert_eq!((trusted.boots(), trusted.latest_received_time()), (4, 500));
assert_eq!(state2.msg_max_size(), 1500);
}
#[test]
fn test_engine_cache_stale_clone_cannot_overwrite_newer_time() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let engine_id = Bytes::from_static(b"engine1");
cache.insert_state(addr, EngineState::new(engine_id.clone(), 7, 500));
cache.insert_state(addr, EngineState::new(engine_id.clone(), 6, 9000));
cache.insert_state(
addr,
EngineState::from_discovery(engine_id, crate::MessageSize::new(1400).unwrap()),
);
let state = cache.get(&addr).unwrap();
let trusted = state.authenticated_time().unwrap();
assert_eq!((trusted.boots(), trusted.latest_received_time()), (7, 500));
}
#[test]
fn test_engine_cache_concurrent_updates_converge_monotonically() {
use std::sync::Arc;
let cache = Arc::new(EngineCache::new());
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let discovered = DiscoveredEngine::new(
Bytes::from_static(b"engine1"),
crate::MessageSize::new(1400).unwrap(),
)
.unwrap();
cache.insert_discovered(addr, discovered.clone()).unwrap();
let older = Arc::clone(&cache);
let newer = Arc::clone(&cache);
let older_engine = discovered.clone();
let newer_engine = discovered;
let older_task = std::thread::spawn(move || {
for _ in 0..100 {
older
.seed_authenticated(addr, older_engine.clone(), 4, 9000)
.unwrap();
}
});
let newer_task = std::thread::spawn(move || {
for _ in 0..100 {
newer
.seed_authenticated(addr, newer_engine.clone(), 5, 10)
.unwrap();
}
});
older_task.join().unwrap();
newer_task.join().unwrap();
let state = cache.get(&addr).unwrap();
let trusted = state.authenticated_time().unwrap();
assert_eq!((trusted.boots(), trusted.latest_received_time()), (5, 10));
}
#[test]
fn test_engine_cache_ttl_expiry() {
let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let now = Instant::now();
let state = EngineState::new(Bytes::from_static(b"engine1"), 1, 1000);
cache.insert_state_at(addr, state, now);
assert!(cache.get_at(&addr, now + Duration::from_secs(5)).is_some());
assert!(
cache.get_at(&addr, now + Duration::from_secs(6)).is_none(),
"expired entry should return None"
);
assert!(cache.is_empty(), "expired entry should be removed");
}
#[test]
fn authenticated_publication_restores_ttl_expired_mapping() {
let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let now = Instant::now();
let engine_id = Bytes::from_static(b"engine1");
let local_state = EngineState::new(engine_id.clone(), 1, 1000);
cache.insert_state_at(addr, local_state.clone(), now);
let outcome = cache.check_and_update_timeliness_at(
&addr,
&local_state,
&engine_id,
1,
1100,
now + Duration::from_secs(6),
);
let TimelinessPublicationOutcome::RestoredMapping(state) = outcome else {
panic!("authenticated publication must restore the expired mapping");
};
assert_eq!(state.engine_id(), &engine_id);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1100
);
assert!(cache.get_at(&addr, now + Duration::from_secs(6)).is_some());
}
#[test]
fn authenticated_publication_restores_capacity_evicted_mapping() {
let cache = EngineCache::new().with_max_capacity(1);
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let other: SocketAddr = "192.0.2.2:161".parse().unwrap();
let now = Instant::now();
let engine_id = Bytes::from_static(b"engine1");
let local_state = EngineState::new(engine_id.clone(), 1, 1000);
cache.insert_state_at(addr, local_state.clone(), now);
cache.insert_state_at(
other,
EngineState::new(Bytes::from_static(b"engine2"), 1, 50),
now + Duration::from_secs(1),
);
assert!(cache.get_at(&addr, now + Duration::from_secs(1)).is_none());
let outcome = cache.check_and_update_timeliness_at(
&addr,
&local_state,
&engine_id,
1,
1100,
now + Duration::from_secs(2),
);
assert!(matches!(
outcome,
TimelinessPublicationOutcome::RestoredMapping(_)
));
assert!(cache.get_at(&other, now + Duration::from_secs(2)).is_none());
assert_eq!(
cache
.get_at(&addr, now + Duration::from_secs(2))
.unwrap()
.authenticated_time()
.unwrap()
.latest_received_time(),
1100
);
}
#[test]
fn authenticated_publication_restores_removed_and_cleared_mappings() {
for clear in [false, true] {
let cache = EngineCache::new();
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let engine_id = Bytes::from_static(b"engine1");
let local_state = EngineState::new(engine_id.clone(), 1, 1000);
cache.insert_state(addr, local_state.clone());
if clear {
cache.clear();
} else {
cache.remove(&addr).expect("seeded mapping");
}
let outcome =
cache.check_and_update_timeliness(&addr, &local_state, &engine_id, 1, 1100);
assert!(matches!(
outcome,
TimelinessPublicationOutcome::RestoredMapping(_)
));
assert_eq!(
cache
.get(&addr)
.unwrap()
.authenticated_time()
.unwrap()
.latest_received_time(),
1100
);
}
}
#[test]
fn capacity_replacement_preserves_same_engine_high_water_time() {
let cache = EngineCache::new().with_max_capacity(1);
let first: SocketAddr = "192.0.2.1:161".parse().unwrap();
let replacement: SocketAddr = "192.0.2.2:161".parse().unwrap();
let now = Instant::now();
let engine_id = Bytes::from_static(b"shared-engine");
cache.insert_state_at(first, EngineState::new(engine_id.clone(), 5, 500), now);
cache.insert_state_at(
replacement,
EngineState::new(engine_id, 4, 9000),
now + Duration::from_secs(1),
);
assert!(cache.get_at(&first, now + Duration::from_secs(1)).is_none());
let state = cache
.get_at(&replacement, now + Duration::from_secs(1))
.unwrap();
let time = state.authenticated_time().unwrap();
assert_eq!((time.boots(), time.latest_received_time()), (5, 500));
}
#[test]
fn concurrent_rebind_and_authenticated_recovery_never_mix_identities() {
use std::sync::{Arc, Barrier};
for _ in 0..64 {
let cache = Arc::new(EngineCache::new());
let addr: SocketAddr = "192.0.2.1:161".parse().unwrap();
let local_id = Bytes::from_static(b"engine-local");
let foreign_id = Bytes::from_static(b"engine-foreign");
let local_state = EngineState::new(local_id.clone(), 1, 1000);
let barrier = Arc::new(Barrier::new(3));
let publishing_cache = Arc::clone(&cache);
let publishing_barrier = Arc::clone(&barrier);
let publishing_state = local_state.clone();
let publishing_id = local_id.clone();
let publisher = std::thread::spawn(move || {
publishing_barrier.wait();
publishing_cache.check_and_update_timeliness(
&addr,
&publishing_state,
&publishing_id,
1,
1100,
)
});
let rebinding_cache = Arc::clone(&cache);
let rebinding_barrier = Arc::clone(&barrier);
let rebinder = std::thread::spawn(move || {
rebinding_barrier.wait();
rebinding_cache.insert_discovered(
addr,
DiscoveredEngine::new(foreign_id, crate::MessageSize::new(1400).unwrap())
.unwrap(),
)
});
barrier.wait();
let publication = publisher.join().unwrap();
let rebind = rebinder.join().unwrap();
let cached = cache.get(&addr).unwrap();
match publication {
TimelinessPublicationOutcome::RestoredMapping(_) => {
assert!(rebind.is_err());
assert_eq!(cached.engine_id(), &local_id);
assert_eq!(
cached.authenticated_time().unwrap().latest_received_time(),
1100
);
}
TimelinessPublicationOutcome::IdentityConflict => {
rebind.unwrap();
assert_eq!(cached.engine_id(), b"engine-foreign".as_slice());
assert!(cached.authenticated_time().is_none());
}
other => panic!("unexpected concurrent publication outcome: {other:?}"),
}
}
}
#[test]
fn test_engine_cache_ttl_refresh_on_every_accepted_authenticated_message() {
let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let now = Instant::now();
let engine_id = Bytes::from_static(b"engine1");
let local_state = EngineState::new(engine_id.clone(), 1, 1000);
cache.insert_state_at(addr, local_state.clone(), now);
let outcome = cache.check_and_update_timeliness_at(
&addr,
&local_state,
&engine_id,
1,
900,
now + Duration::from_secs(4),
);
assert!(matches!(
outcome,
TimelinessPublicationOutcome::Published(_)
));
assert!(
cache.get_at(&addr, now + Duration::from_secs(8)).is_some(),
"accepted authenticated input must refresh TTL without advancing high-water"
);
}
#[test]
fn test_engine_cache_live_state_prevents_rebuilt_cache_from_accepting_old_boots() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let now = Instant::now();
let engine_id = Bytes::from_static(b"engine1");
let mut local_state = EngineState::new(engine_id.clone(), 5, 1000);
local_state.authenticated_time.as_mut().unwrap().received_at = now;
cache.insert_state_at(
addr,
EngineState::from_discovery(engine_id.clone(), crate::MessageSize::new(1400).unwrap()),
now,
);
let outcome = cache.check_and_update_timeliness_at(
&addr,
&local_state,
&engine_id,
4,
5000,
now + Duration::from_secs(1),
);
let TimelinessPublicationOutcome::Stale(canonical) = outcome else {
panic!("rebuilt cache must reject stale input");
};
let trusted = canonical.authenticated_time().unwrap();
assert_eq!((trusted.boots(), trusted.latest_received_time()), (5, 1000));
}
#[test]
fn test_engine_cache_rejected_message_does_not_refresh_existing_entry() {
let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let now = Instant::now();
let engine_id = Bytes::from_static(b"engine1");
let mut local_state = EngineState::new(engine_id.clone(), 5, 1000);
local_state.authenticated_time.as_mut().unwrap().received_at = now;
cache.insert_state_at(addr, local_state.clone(), now);
let outcome = cache.check_and_update_timeliness_at(
&addr,
&local_state,
&engine_id,
4,
5000,
now + Duration::from_secs(4),
);
assert!(matches!(outcome, TimelinessPublicationOutcome::Stale(_)));
assert!(cache.get_at(&addr, now + Duration::from_secs(6)).is_none());
}
#[test]
fn test_engine_cache_rejected_message_does_not_resurrect_expired_entry() {
let cache = EngineCache::new().with_ttl(Duration::from_secs(5));
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
let now = Instant::now();
let engine_id = Bytes::from_static(b"engine1");
let mut local_state = EngineState::new(engine_id.clone(), 5, 1000);
local_state.authenticated_time.as_mut().unwrap().received_at = now;
cache.insert_state_at(addr, local_state.clone(), now);
let outcome = cache.check_and_update_timeliness_at(
&addr,
&local_state,
&engine_id,
4,
5000,
now + Duration::from_secs(6),
);
assert!(matches!(outcome, TimelinessPublicationOutcome::Stale(_)));
assert!(cache.get_at(&addr, now + Duration::from_secs(6)).is_none());
assert!(cache.is_empty());
}
#[test]
fn test_engine_cache_max_capacity_eviction() {
let cache = EngineCache::new().with_max_capacity(2);
let addr1: SocketAddr = "192.168.1.1:161".parse().unwrap();
let addr2: SocketAddr = "192.168.1.2:161".parse().unwrap();
let addr3: SocketAddr = "192.168.1.3:161".parse().unwrap();
let now = Instant::now();
cache.insert_state_at(
addr1,
EngineState::new(Bytes::from_static(b"e1"), 1, 100),
now,
);
cache.insert_state_at(
addr2,
EngineState::new(Bytes::from_static(b"e2"), 1, 200),
now + Duration::from_secs(1),
);
assert_eq!(cache.len(), 2);
cache.insert_state_at(
addr3,
EngineState::new(Bytes::from_static(b"e3"), 1, 300),
now + Duration::from_secs(2),
);
assert_eq!(cache.len(), 2);
assert!(
cache.get(&addr1).is_none(),
"oldest entry should be evicted"
);
assert!(cache.get(&addr2).is_some());
assert!(cache.get(&addr3).is_some());
}
fn raw_usm_with_engine_id(engine_id: &[u8]) -> Bytes {
let mut buf = crate::ber::EncodeBuf::new();
buf.push_sequence(|buf| {
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_octet_string(&[])?;
buf.push_integer(1);
buf.push_integer(1);
buf.push_octet_string(engine_id)
})
.unwrap();
buf.finish()
}
#[test]
fn test_parse_discovery_response() {
let usm = UsmSecurityParams::new(b"test-engine-id".as_slice(), 42, 12345, b"".as_slice())
.unwrap();
let encoded = usm.encode().unwrap();
let state = parse_discovery_response(&encoded).unwrap();
assert_eq!(state.engine_id.as_ref(), b"test-engine-id");
assert_eq!(state.msg_max_size, UDP_RECEIVE_LIMITS.advertised());
}
#[test]
fn test_discovered_engine_rejects_invalid_identity() {
let capacity = crate::MessageSize::new(1400).unwrap();
assert!(DiscoveredEngine::new(Bytes::new(), capacity).is_err());
assert!(DiscoveredEngine::new(Bytes::from_static(b"valid"), capacity).is_ok());
assert!(DiscoveredEngine::new(Bytes::from_static(&[0; 5]), capacity).is_err());
assert!(DiscoveredEngine::new(Bytes::from_static(&[0xff; 5]), capacity).is_err());
assert!(DiscoveredEngine::new(Bytes::from(vec![1; 33]), capacity).is_err());
}
#[test]
fn test_parse_discovery_response_empty_engine_id() {
let usm = UsmSecurityParams::discovery();
let encoded = usm.encode().unwrap();
let result = parse_discovery_response(&encoded);
assert!(matches!(*result.unwrap_err(), Error::InvalidMessage(_)));
}
#[test]
fn test_parse_discovery_response_rejects_invalid_engine_id() {
for engine_id in [
b"abcd".as_slice(),
[0_u8; 8].as_slice(),
[0xff_u8; 8].as_slice(),
[1_u8; 33].as_slice(),
] {
assert!(matches!(
*parse_discovery_response(&raw_usm_with_engine_id(engine_id)).unwrap_err(),
Error::InvalidMessage(_)
));
}
}
#[test]
fn test_engine_boots_transition_to_max() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_646, 1000);
assert!(
state.update_time(2_147_483_647, 100),
"Transition to boots=2_147_483_647 should be accepted"
);
assert_eq!(state.authenticated_time().unwrap().boots(), 2_147_483_647);
assert_eq!(
state.authenticated_time().unwrap().received_time_base(),
100
);
}
#[test]
fn test_engine_boots_latched_update_behavior() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 1000);
assert!(
state.update_time(2_147_483_647, 2000),
"Time tracking updates should still work"
);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
2000
);
assert!(!state.update_time(2_147_483_647, 1500));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
2000
);
assert!(
!state.is_in_time_window(2_147_483_647, 2000),
"Latched state should still reject all messages"
);
}
#[test]
fn test_engine_boots_latched_time_window_always_fails() {
let state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 1000);
assert!(!state.is_in_time_window(2_147_483_647, 0));
assert!(!state.is_in_time_window(2_147_483_647, 1000));
assert!(!state.is_in_time_window(2_147_483_647, 1001));
assert!(!state.is_in_time_window(2_147_483_647, u32::MAX));
assert!(!state.is_in_time_window(2_147_483_646, 1000));
assert!(!state.is_in_time_window(0, 1000));
}
#[test]
fn test_engine_state_created_latched() {
let state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_647, 5000);
assert_eq!(state.authenticated_time().unwrap().boots(), 2_147_483_647);
assert_eq!(
state.authenticated_time().unwrap().received_time_base(),
5000
);
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
5000
);
assert!(
!state.is_in_time_window(2_147_483_647, 5000),
"Newly created latched engine should reject all messages"
);
}
#[test]
fn test_engine_boots_near_max_operates_normally() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_645, 1000);
assert!(state.is_in_time_window(2_147_483_645, 1000));
assert!(state.is_in_time_window(2_147_483_645, 1100));
assert!(!state.is_in_time_window(2_147_483_645, 1200));
assert!(state.update_time(2_147_483_646, 500));
assert_eq!(state.authenticated_time().unwrap().boots(), 2_147_483_646);
assert!(state.is_in_time_window(2_147_483_646, 500));
assert!(state.update_time(2_147_483_647, 100));
assert_eq!(state.authenticated_time().unwrap().boots(), 2_147_483_647);
assert!(!state.is_in_time_window(2_147_483_647, 100));
}
#[test]
fn test_engine_boots_high_value_update_logic() {
let mut state = EngineState::new(Bytes::from_static(b"engine"), 2_147_483_640, 1000);
assert!(!state.update_time(2147483639, 9999));
assert!(!state.update_time(0, 9999));
assert!(!state.update_time(2_147_483_640, 500));
assert!(state.update_time(2_147_483_640, 1500));
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
1500
);
assert!(state.update_time(2_147_483_641, 100));
assert_eq!(state.authenticated_time().unwrap().boots(), 2_147_483_641);
}
#[test]
fn test_engine_cache_latched_engine() {
let cache = EngineCache::new();
let addr: SocketAddr = "192.168.1.1:161".parse().unwrap();
cache.insert_state(
addr,
EngineState::new(Bytes::from_static(b"latched"), 2_147_483_647, 1000),
);
assert!(
cache.update_time(&addr, 2_147_483_647, 2000),
"Time tracking should update even for latched engine"
);
let state = cache.get(&addr).unwrap();
assert_eq!(
state.authenticated_time().unwrap().latest_received_time(),
2000
);
assert!(
!state.is_in_time_window(2_147_483_647, 2000),
"Latched engine should reject all time window checks"
);
}
#[test]
fn test_engine_state_stores_msg_max_size() {
let state = EngineState::with_msg_max_size(
Bytes::from_static(b"engine"),
1,
1000,
crate::MessageSize::new(65507).unwrap(),
);
assert_eq!(state.msg_max_size(), 65507);
}
#[test]
fn test_engine_state_default_msg_max_size() {
let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
assert_eq!(
state.msg_max_size(),
UDP_RECEIVE_LIMITS.advertised(),
"Default msg_max_size should be the maximum UDP datagram size"
);
}
#[test]
fn test_engine_state_msg_max_size_capped_to_session_max() {
let state = EngineState::with_msg_max_size_capped(
Bytes::from_static(b"engine"),
1,
1000,
MessageSize::new(2_000_000_000).unwrap(), UDP_RECEIVE_LIMITS.advertised(), );
assert_eq!(
state.msg_max_size(),
65507,
"msg_max_size should be capped to session maximum"
);
}
#[test]
fn test_engine_state_msg_max_size_within_limit_not_capped() {
let state = EngineState::with_msg_max_size_capped(
Bytes::from_static(b"engine"),
1,
1000,
MessageSize::new(1472).unwrap(), UDP_RECEIVE_LIMITS.advertised(), );
assert_eq!(
state.msg_max_size(),
1472,
"msg_max_size within limit should not be capped"
);
}
#[test]
fn test_engine_state_msg_max_size_at_exact_boundary() {
let state = EngineState::with_msg_max_size_capped(
Bytes::from_static(b"engine"),
1,
1000,
UDP_RECEIVE_LIMITS.advertised(), UDP_RECEIVE_LIMITS.advertised(), );
assert_eq!(state.msg_max_size(), 65507);
}
#[test]
fn discovery_preserves_remote_capacity_independently_of_local_limits() {
let params =
UsmSecurityParams::new(Bytes::from_static(b"remote-engine"), 0, 0, Bytes::new())
.unwrap()
.encode()
.unwrap();
let reported = MessageSize::new(9000).unwrap();
let state = parse_discovery_response_with_msg_max_size(¶ms, reported).unwrap();
assert_eq!(state.msg_max_size, reported);
let capped = parse_discovery_response_with_limits(
¶ms,
reported,
MessageSize::new(1400).unwrap(),
)
.unwrap();
assert_eq!(capped.msg_max_size, 1400);
}
#[test]
fn test_engine_state_msg_max_size_tcp_limit() {
const TCP_MAX: u32 = 0x7FFF_FFFF;
let state = EngineState::with_msg_max_size_capped(
Bytes::from_static(b"engine"),
1,
1000,
MessageSize::try_from(TCP_MAX).unwrap(),
MessageSize::try_from(TCP_MAX).unwrap(),
);
assert_eq!(state.msg_max_size(), TCP_MAX);
assert!(MessageSize::try_from(u32::MAX).is_err());
}
#[test]
fn test_engine_state_new_uses_default_constant() {
let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
assert_eq!(state.msg_max_size(), UDP_RECEIVE_LIMITS.advertised());
}
#[test]
fn test_estimated_time_caps_at_max_engine_time() {
let state = EngineState::new(Bytes::from_static(b"engine"), 1, MAX_ENGINE_TIME - 10);
let estimated = state.estimated_boots_time().1;
assert!(
estimated <= MAX_ENGINE_TIME,
"estimated_time() should never exceed MAX_ENGINE_TIME ({MAX_ENGINE_TIME}), got {estimated}"
);
}
#[test]
fn test_estimated_pair_rolls_after_max_engine_time() {
let now = Instant::now();
let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, 0);
state.authenticated_time.as_mut().unwrap().received_at = now;
assert_eq!(
state.estimated_boots_time_at(now + Duration::from_secs(u64::from(MAX_ENGINE_TIME))),
(1, MAX_ENGINE_TIME)
);
assert_eq!(
state
.estimated_boots_time_at(now + Duration::from_secs(u64::from(MAX_ENGINE_TIME) + 1)),
(2, 0)
);
}
#[test]
fn test_max_engine_time_tuple_remains_timely() {
let now = Instant::now();
let mut state = EngineState::new(Bytes::from_static(b"engine"), 1, MAX_ENGINE_TIME);
state.authenticated_time.as_mut().unwrap().received_at = now;
assert!(state.check_and_update_timeliness_at(1, MAX_ENGINE_TIME, now));
assert_eq!(state.estimated_boots_time_at(now), (1, MAX_ENGINE_TIME));
}
#[test]
fn test_max_engine_time_constant() {
assert_eq!(MAX_ENGINE_TIME, 2_147_483_647);
assert_eq!(MAX_ENGINE_TIME, i32::MAX as u32);
}
#[test]
fn test_estimated_time_normal_operation() {
let state = EngineState::new(Bytes::from_static(b"engine"), 1, 1000);
let estimated = state.estimated_boots_time().1;
assert!(
estimated >= 1000,
"estimated_time() should be at least engine_time"
);
assert!(
estimated < MAX_ENGINE_TIME,
"Normal time values should not hit MAX_ENGINE_TIME cap"
);
}
}