prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::engine::InstantMillis;
use crate::identity::IdentityHash;
use crate::interfaces::{InboundPacket, InterfaceId, InterfaceKind};
use crate::routing::announce::Announce;
use crate::routing::dedup::PacketHash;
use crate::routing::NextHop;
use crate::wire::{
    IfacFlag, PacketType, WireAddress, WireContext, WirePacketHeader, BROADCAST_MTU,
};

#[derive(Debug, PartialEq, Eq)]
pub struct DataPacket<'a> {
    pub header: WirePacketHeader,
    pub payload: &'a mut [u8],
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Ingress<'a> {
    Announce {
        packet_hash: PacketHash,
        identity_hash: IdentityHash,
        announce: Announce<'a>,
        payload: &'a [u8],
        header: WirePacketHeader,
        received_hops: u8,
        source_interface: InterfaceId,
        arrived_at: InstantMillis,
        next_hop: NextHop,
        is_path_response: bool,
    },

    Data {
        packet_hash: PacketHash,
        data: DataPacket<'a>,
        received_hops: u8,
        source_interface: InterfaceId,
        arrived_at: InstantMillis,
    },

    LinkRequest {
        packet_hash: PacketHash,
        payload: &'a [u8],
        header: WirePacketHeader,
        received_hops: u8,
        source_interface: InterfaceId,
        arrived_at: InstantMillis,
    },

    Proof {
        packet_hash: PacketHash,
        payload: &'a [u8],
        address: WireAddress,
        context: WireContext,
        received_hops: u8,
        source_interface: InterfaceId,
        arrived_at: InstantMillis,
    },

    Malformed,
    IfacRefused,
}

#[derive(Debug)]
pub struct ClassifiedInboundPacket<'a> {
    source_interface: InterfaceId,
    ingress: Ingress<'a>,
}

impl<'a> ClassifiedInboundPacket<'a> {
    #[must_use]
    pub fn classify(packet: InboundPacket<'a>) -> Self {
        let source_interface = packet.source_interface;
        Self {
            source_interface,
            ingress: Ingress::classify(packet),
        }
    }

    #[must_use]
    pub fn packet_hash(&self) -> Option<PacketHash> {
        self.ingress.packet_hash()
    }

    #[must_use]
    pub fn proof(&self) -> Option<(WireAddress, &[u8])> {
        match &self.ingress {
            Ingress::Proof {
                address, payload, ..
            } => Some((*address, *payload)),
            Ingress::Announce { .. }
            | Ingress::Data { .. }
            | Ingress::LinkRequest { .. }
            | Ingress::Malformed
            | Ingress::IfacRefused => None,
        }
    }

    pub(crate) fn into_parts(self) -> (InterfaceId, Ingress<'a>) {
        (self.source_interface, self.ingress)
    }
}

fn local_adjusted_hops(received_hops: u8, source: InterfaceId) -> u8 {
    if source.kind() == Some(InterfaceKind::LocalClient) {
        received_hops.saturating_sub(1)
    } else {
        received_hops
    }
}

impl<'a> Ingress<'a> {
    #[must_use]
    pub fn packet_hash(&self) -> Option<PacketHash> {
        match self {
            Self::Announce { packet_hash, .. }
            | Self::Data { packet_hash, .. }
            | Self::LinkRequest { packet_hash, .. }
            | Self::Proof { packet_hash, .. } => Some(*packet_hash),
            Self::Malformed | Self::IfacRefused => None,
        }
    }

    pub fn classify(packet: InboundPacket<'a>) -> Self {
        let InboundPacket {
            arrived_at,
            source_interface,
            bytes,
        } = packet;
        let (header, payload_offset) = match WirePacketHeader::parse(bytes) {
            Ok((header, payload)) => (header, bytes.len() - payload.len()),
            Err(_) => return Self::Malformed,
        };
        if header.ifac_flag == IfacFlag::Authenticated {
            return Self::IfacRefused;
        }
        let (_, payload) = bytes.split_at_mut(payload_offset);
        let packet_hash = PacketHash::of_fields(
            header.destination_type,
            header.packet_type,
            &header.address,
            header.context,
            payload,
        );

        let received_hops = local_adjusted_hops(header.hops.saturating_add(1), source_interface);

        match header.packet_type {
            PacketType::Announce => {
                let payload: &'a [u8] = payload;
                let Ok((announce, identity_hash)) =
                    Announce::from_wire_unverified_with_identity(&header, payload)
                else {
                    return Self::Malformed;
                };

                debug_assert!(
                    {
                        let mut scratch = [0u8; BROADCAST_MTU];
                        announce
                            .to_wire(&mut scratch)
                            .map(|n| &scratch[..n] == payload)
                            .unwrap_or(false)
                    },
                    "Announce::to_wire∘from_wire must round-trip, else a rebroadcast re-emits a signature-broken packet"
                );

                Self::Announce {
                    packet_hash,
                    identity_hash,
                    announce,
                    payload,
                    header,
                    received_hops,
                    source_interface,
                    arrived_at,
                    next_hop: header.transport_id.map_or(NextHop::Direct, NextHop::Via),
                    is_path_response: header.context == WireContext::PathResponse,
                }
            }
            PacketType::Data => Self::Data {
                packet_hash,
                data: DataPacket { header, payload },
                received_hops,
                source_interface,
                arrived_at,
            },
            PacketType::LinkRequest => Self::LinkRequest {
                packet_hash,
                payload,
                header,
                received_hops,
                source_interface,
                arrived_at,
            },
            PacketType::Proof => Self::Proof {
                packet_hash,
                payload,
                address: header.address,
                context: header.context,
                received_hops,
                source_interface,
                arrived_at,
            },
        }
    }
}

#[cfg(test)]
mod tests;