prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::routing::dedup::{
    PacketHash, PacketHashHistory, RememberPacketOutcome, PACKET_HASH_LEN,
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FixedPacketHashHistory<const GENERATION_CAPACITY: usize> {
    current_len: usize,
    previous_len: usize,
    current: [PacketHash; GENERATION_CAPACITY],
    previous: [PacketHash; GENERATION_CAPACITY],
}

impl<const GENERATION_CAPACITY: usize> Default for FixedPacketHashHistory<GENERATION_CAPACITY> {
    fn default() -> Self {
        Self {
            current_len: 0,
            previous_len: 0,
            current: [PacketHash::new([0u8; PACKET_HASH_LEN]); GENERATION_CAPACITY],
            previous: [PacketHash::new([0u8; PACKET_HASH_LEN]); GENERATION_CAPACITY],
        }
    }
}

impl<const GENERATION_CAPACITY: usize> PacketHashHistory
    for FixedPacketHashHistory<GENERATION_CAPACITY>
{
    fn generation_capacity(&self) -> usize {
        GENERATION_CAPACITY
    }

    fn len(&self) -> usize {
        self.current_len + self.previous_len
    }

    fn contains(&self, hash: &PacketHash) -> bool {
        self.current[..self.current_len].contains(hash)
            || self.previous[..self.previous_len].contains(hash)
    }

    fn remember(&mut self, hash: PacketHash) -> RememberPacketOutcome {
        if self.contains(&hash) {
            return RememberPacketOutcome::AlreadyKnown;
        }

        if self.current_len < GENERATION_CAPACITY {
            self.current[self.current_len] = hash;
            self.current_len += 1;
            return RememberPacketOutcome::StoredFresh;
        }

        core::mem::swap(&mut self.current, &mut self.previous);
        self.previous_len = self.current_len;
        self.current[0] = hash;
        self.current_len = 1;
        RememberPacketOutcome::StoredAfterRotation
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn hash(byte: u8) -> PacketHash {
        PacketHash::new([byte; PACKET_HASH_LEN])
    }

    #[test]
    fn remembers_across_both_generations_and_reports_duplicates() {
        let mut history = FixedPacketHashHistory::<2>::default();
        assert!(history.is_empty());

        assert_eq!(
            history.remember(hash(1)),
            RememberPacketOutcome::StoredFresh
        );
        assert_eq!(
            history.remember(hash(2)),
            RememberPacketOutcome::StoredFresh
        );
        assert_eq!(
            history.remember(hash(1)),
            RememberPacketOutcome::AlreadyKnown
        );
        assert_eq!(history.len(), 2);

        assert_eq!(
            history.remember(hash(3)),
            RememberPacketOutcome::StoredAfterRotation,
        );
        assert!(history.contains(&hash(1)));
        assert!(history.contains(&hash(2)));
        assert!(history.contains(&hash(3)));
        assert_eq!(history.len(), 3);
    }

    #[test]
    fn two_rotations_forget_the_oldest_generation() {
        let mut history = FixedPacketHashHistory::<2>::default();
        for byte in 1..=2 {
            let _ = history.remember(hash(byte));
        }
        assert_eq!(
            history.remember(hash(3)),
            RememberPacketOutcome::StoredAfterRotation,
        );
        let _ = history.remember(hash(4));
        assert_eq!(
            history.remember(hash(5)),
            RememberPacketOutcome::StoredAfterRotation,
        );

        assert!(!history.contains(&hash(1)));
        assert!(!history.contains(&hash(2)));
        assert!(history.contains(&hash(3)));
        assert!(history.contains(&hash(4)));
        assert!(history.contains(&hash(5)));
    }

    #[test]
    fn a_duplicate_in_the_previous_generation_is_still_known() {
        let mut history = FixedPacketHashHistory::<2>::default();
        let _ = history.remember(hash(1));
        let _ = history.remember(hash(2));
        let _ = history.remember(hash(3));
        assert_eq!(
            history.remember(hash(1)),
            RememberPacketOutcome::AlreadyKnown
        );
    }
}