prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::interfaces::{InterfaceDescriptor, InterfaceId};
#[cfg(feature = "alloc")]
use crate::lemire_index::HeapLemireIndex;

#[derive(Debug, Clone, Copy)]
pub struct AttachedInterfaces<'a> {
    descriptors: &'a [InterfaceDescriptor],
    #[cfg(feature = "alloc")]
    index: Option<InterfaceIndex<'a>>,
}

#[cfg(feature = "alloc")]
#[derive(Debug, Clone, Copy)]
struct InterfaceIndex<'a> {
    ids: &'a [InterfaceId],
    index: &'a HeapLemireIndex,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Egress {
    Transmit,
    Transport,
}

impl<'a> AttachedInterfaces<'a> {
    pub const fn new(descriptors: &'a [InterfaceDescriptor]) -> Self {
        Self {
            descriptors,
            #[cfg(feature = "alloc")]
            index: None,
        }
    }

    pub fn descriptor_for(self, id: InterfaceId) -> Option<&'a InterfaceDescriptor> {
        #[cfg(feature = "alloc")]
        if let Some(indexed) = self.index {
            return indexed
                .index
                .get(&id, indexed.ids)
                .map(|row| &self.descriptors[row]);
        }
        self.descriptors
            .iter()
            .find(|descriptor| descriptor.id == id)
    }

    pub fn is_egress_eligible(self, target: InterfaceId, egress_kind: Egress) -> bool {
        self.descriptor_for(target)
            .is_some_and(|descriptor| match egress_kind {
                Egress::Transmit => descriptor.capabilities.allows_transmit(),
                Egress::Transport => descriptor.capabilities.allows_transport(),
            })
    }

    pub const fn len(self) -> usize {
        self.descriptors.len()
    }

    pub const fn is_empty(self) -> bool {
        self.descriptors.is_empty()
    }

    pub fn iter(self) -> core::slice::Iter<'a, InterfaceDescriptor> {
        self.descriptors.iter()
    }
}

impl<'a> IntoIterator for AttachedInterfaces<'a> {
    type Item = &'a InterfaceDescriptor;
    type IntoIter = core::slice::Iter<'a, InterfaceDescriptor>;

    fn into_iter(self) -> Self::IntoIter {
        self.descriptors.iter()
    }
}

#[cfg(feature = "alloc")]
#[derive(Debug, Default)]
pub struct IndexedAttachedInterfaces {
    descriptors: alloc::vec::Vec<InterfaceDescriptor>,
    ids: alloc::vec::Vec<InterfaceId>,
    index: HeapLemireIndex,
}

#[cfg(feature = "alloc")]
impl IndexedAttachedInterfaces {
    pub fn view(&self) -> AttachedInterfaces<'_> {
        AttachedInterfaces {
            descriptors: &self.descriptors,
            index: Some(InterfaceIndex {
                ids: &self.ids,
                index: &self.index,
            }),
        }
    }

    pub fn descriptors(&self) -> &[InterfaceDescriptor] {
        &self.descriptors
    }

    pub fn push(&mut self, descriptor: InterfaceDescriptor) {
        self.ids.push(descriptor.id);
        self.descriptors.push(descriptor);
        self.index.insert(self.ids.len() - 1, &self.ids);
    }

    pub fn remove(&mut self, id: InterfaceId) {
        let Some(row) = self.index.get(&id, &self.ids) else {
            return;
        };
        self.index.remove(&id, &self.ids);
        let last = self.ids.len() - 1;
        if row != last {
            self.index.repoint(&self.ids[last], row, &self.ids);
        }
        self.ids.swap_remove(row);
        self.descriptors.swap_remove(row);
    }
}

#[cfg(feature = "alloc")]
impl From<alloc::vec::Vec<InterfaceDescriptor>> for IndexedAttachedInterfaces {
    fn from(descriptors: alloc::vec::Vec<InterfaceDescriptor>) -> Self {
        let mut roster = Self::default();
        for descriptor in descriptors {
            roster.push(descriptor);
        }
        roster
    }
}

#[cfg(all(test, feature = "alloc"))]
mod tests {
    use super::*;
    use crate::interfaces::{
        AnnounceBandwidthCap, BitrateBps, EgressCapability, IngressCapability,
        InterfaceCapabilities, InterfaceCommonPolicy, InterfaceMode, TransportCapability,
    };

    fn descriptor(n: u32) -> InterfaceDescriptor {
        let mut id = [0x07u8, 0, 0, 0, 0, 0, 0, 0];
        id[4..].copy_from_slice(&n.to_be_bytes());
        InterfaceDescriptor {
            id: InterfaceId::new(id),
            capabilities: InterfaceCapabilities {
                ingress: IngressCapability::Enabled,
                egress: EgressCapability::Enabled(TransportCapability::CrossInterfaceOnly),
            },
            mode: InterfaceMode::Full,
            gravity: crate::interfaces::InterfaceGravity::ZERO,
            bitrate: BitrateBps::guess(1_000_000_000),
            hardware_mtu: None,
            announce_rate_limit: None,
            announce_bandwidth_cap: AnnounceBandwidthCap::Unlimited,
            airtime_duty_cycle: None,
            common: InterfaceCommonPolicy::RNS_DEFAULT,
        }
    }

    #[test]
    fn an_indexed_view_answers_like_the_linear_scan_through_churn() {
        let mut roster = IndexedAttachedInterfaces::default();
        let mut live: std::vec::Vec<u32> = std::vec::Vec::new();
        let mut rng = 0x0123_4567_89AB_CDEFu64;
        let mut next_id = 0u32;

        for _ in 0..1_000 {
            rng = rng
                .wrapping_mul(6364136223846793005)
                .wrapping_add(1442695040888963407);
            let attach = live.len() < 2 || !(rng >> 33).is_multiple_of(3);
            if attach {
                roster.push(descriptor(next_id));
                live.push(next_id);
                next_id += 1;
            } else {
                let victim = ((rng >> 17) as usize) % live.len();
                roster.remove(descriptor(live[victim]).id);
                live.swap_remove(victim);
            }

            let view = roster.view();
            for &n in &live {
                let id = descriptor(n).id;
                assert_eq!(
                    view.descriptor_for(id).map(|found| found.id),
                    Some(id),
                    "every live interface resolves through the indexed view",
                );
            }
            assert!(view.descriptor_for(descriptor(next_id + 7).id).is_none());
        }
        assert!(
            live.len() > 50,
            "the run must grow enough to force reindexing"
        );
    }

    #[test]
    fn removing_an_unknown_id_leaves_the_roster_intact() {
        let mut roster = IndexedAttachedInterfaces::from(std::vec![descriptor(1), descriptor(2)]);
        roster.remove(descriptor(9).id);
        assert_eq!(roster.descriptors().len(), 2);
        assert!(roster
            .view()
            .is_egress_eligible(descriptor(2).id, Egress::Transport));
    }
}