prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use super::*;
use crate::engine::test_support::*;
use crate::routing::ingress::testkit::{header_bytes, iface};
use crate::wire::{ContextFlag, DestinationType, PropagationType, TransportId, HEADER_MIN_LEN};

#[test]
fn a_local_client_transit_is_discounted_one_hop() {
    let local_client = InterfaceId::from_channel_tag(InterfaceKind::LocalClient, b"app-1");
    let tcp = InterfaceId::from_channel_tag(InterfaceKind::TcpClient, b"1.2.3.4:4242");
    assert_eq!(local_adjusted_hops(5, local_client), 4);
    assert_eq!(local_adjusted_hops(5, tcp), 5);
    assert_eq!(local_adjusted_hops(0, local_client), 0);
}

#[test]
fn an_ifac_flagged_packet_is_dropped_at_the_door_like_rns_on_a_non_ifac_interface() {
    let mut raw = crate::engine::test_support::bytes_from_hex(
        crate::engine::test_support::RNS_1_4_2_ANNOUNCE,
    );
    raw[0] |= 0x80;
    let packet = InboundPacket {
        arrived_at: InstantMillis(7),
        source_interface: iface(0x01),
        bytes: &mut raw,
    };

    assert!(matches!(Ingress::classify(packet), Ingress::IfacRefused));
}

#[test]
fn malformed_headers_classify_as_malformed() {
    let packet = InboundPacket {
        arrived_at: InstantMillis(7),
        source_interface: iface(0x01),
        bytes: &mut [0x01],
    };

    assert!(matches!(Ingress::classify(packet), Ingress::Malformed));
}

#[test]
fn rejected_packets_never_expose_a_canonical_hash() {
    let mut malformed_bytes = [0x01];
    let malformed = ClassifiedInboundPacket::classify(InboundPacket {
        arrived_at: InstantMillis(7),
        source_interface: iface(0x01),
        bytes: &mut malformed_bytes,
    });
    assert_eq!(malformed.packet_hash(), None);

    let mut ifac = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
    ifac[0] |= 0x80;
    let refused = ClassifiedInboundPacket::classify(InboundPacket {
        arrived_at: InstantMillis(7),
        source_interface: iface(0x01),
        bytes: &mut ifac,
    });
    assert_eq!(refused.packet_hash(), None);
}

#[test]
fn classified_proof_exposes_its_parsed_fast_path_view() {
    let mut bytes = header_bytes(PacketType::Proof);
    let expected_address = WirePacketHeader::parse(&bytes).unwrap().0.address;
    let classified = ClassifiedInboundPacket::classify(InboundPacket {
        arrived_at: InstantMillis(9),
        source_interface: iface(0x02),
        bytes: &mut bytes,
    });

    assert_eq!(classified.proof(), Some((expected_address, &[][..])));
}

#[test]
fn recognized_non_announce_packets_classify_from_the_header() {
    for packet_type in [PacketType::Data, PacketType::LinkRequest, PacketType::Proof] {
        let mut bytes = header_bytes(packet_type);
        let packet = InboundPacket {
            arrived_at: InstantMillis(9),
            source_interface: iface(0x02),
            bytes: &mut bytes,
        };

        let classified = Ingress::classify(packet);
        match packet_type {
            PacketType::Data => assert!(matches!(classified, Ingress::Data { .. })),
            PacketType::LinkRequest => {
                assert!(matches!(classified, Ingress::LinkRequest { .. }))
            }
            PacketType::Proof => assert!(matches!(classified, Ingress::Proof { .. })),
            PacketType::Announce => unreachable!(),
        }
    }
}

#[test]
fn data_packets_carry_their_typed_fields_through_classification() {
    let header = WirePacketHeader {
        ifac_flag: IfacFlag::Open,
        context_flag: ContextFlag::Unset,
        propagation: PropagationType::Transport,
        destination_type: DestinationType::Plain,
        packet_type: PacketType::Data,
        hops: 5,
        transport_id: Some(TransportId::new([0x11; 16])),
        address: WireAddress::new([0xA5; 16]),
        context: WireContext::Resource,
    };
    let payload = [0xDE, 0xAD, 0xBE, 0xEF];
    let mut expected_payload = payload;
    let mut bytes = [0u8; BROADCAST_MTU];
    let header_len = header.write(&mut bytes).unwrap();
    bytes[header_len..header_len + payload.len()].copy_from_slice(&payload);
    let packet_len = header_len + payload.len();
    let expected_hash = PacketHash::of_wire_packet(&bytes[..packet_len]).unwrap();

    let packet = InboundPacket {
        arrived_at: InstantMillis(21),
        source_interface: iface(0x05),
        bytes: &mut bytes[..packet_len],
    };
    let classified = ClassifiedInboundPacket::classify(packet);
    assert_eq!(classified.packet_hash(), Some(expected_hash));
    let (classified_source, ingress) = classified.into_parts();

    let Ingress::Data {
        packet_hash,
        data,
        received_hops,
        source_interface,
        arrived_at,
    } = ingress
    else {
        panic!("a data packet should classify as data");
    };
    assert_eq!(
        data,
        DataPacket {
            header,
            payload: &mut expected_payload,
        }
    );
    assert_eq!(received_hops, 6);
    assert_eq!(classified_source, iface(0x05));
    assert_eq!(source_interface, iface(0x05));
    assert_eq!(arrived_at, InstantMillis(21));
    assert_eq!(packet_hash, expected_hash);
}

#[test]
fn data_packets_classify_for_every_destination_type() {
    for destination_type in [
        DestinationType::Single,
        DestinationType::Group,
        DestinationType::Plain,
        DestinationType::Link,
    ] {
        let header = WirePacketHeader {
            ifac_flag: IfacFlag::Open,
            context_flag: ContextFlag::Unset,
            propagation: PropagationType::Broadcast,
            destination_type,
            packet_type: PacketType::Data,
            hops: 0,
            transport_id: None,
            address: WireAddress::new([0xA5; 16]),
            context: WireContext::None,
        };
        let mut bytes = [0u8; HEADER_MIN_LEN];
        assert_eq!(header.write(&mut bytes).unwrap(), HEADER_MIN_LEN);
        let packet = InboundPacket {
            arrived_at: InstantMillis(23),
            source_interface: iface(0x06),
            bytes: &mut bytes,
        };

        let Ingress::Data { data, .. } = Ingress::classify(packet) else {
            panic!("data packets to any destination type classify as data");
        };
        assert_eq!(data.header.destination_type, destination_type);
        assert!(data.payload.is_empty());
    }
}

#[test]
fn announce_packets_must_target_a_single_destination() {
    let mut raw = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
    raw[0] |= (DestinationType::Group as u8) << 2;
    let packet = InboundPacket {
        arrived_at: InstantMillis(11),
        source_interface: iface(0x03),
        bytes: &mut raw,
    };

    assert!(matches!(Ingress::classify(packet), Ingress::Malformed));
}

#[test]
fn announce_received_hops_saturates_at_wire_max() {
    let mut raw = bytes_from_hex(RNS_1_4_2_ANNOUNCE);
    raw[1] = u8::MAX;
    let source_interface = iface(0x04);
    let arrived_at = InstantMillis(13);
    let packet = InboundPacket {
        arrived_at,
        source_interface,
        bytes: &mut raw,
    };

    let classified = Ingress::classify(packet);
    let Ingress::Announce {
        received_hops,
        source_interface: classified_source,
        arrived_at: classified_arrival,
        ..
    } = classified
    else {
        panic!("valid announce should classify as announce");
    };
    assert_eq!(received_hops, u8::MAX);
    assert_eq!(classified_source, source_interface);
    assert_eq!(classified_arrival, arrived_at);
}