prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
#![allow(clippy::unwrap_used)]

use super::{
    DeviceEvent, EndpointId, MultipathDeduplicator, SwitchId, WeaveHostIdentity, BROADCAST_SWITCH,
};
use crate::interfaces::rns_serial_framing::{self, RnsSerialDecoder};
use crate::interfaces::weave::{WDCL_MAX_CHUNK, WEAVE_MAX_WIRE_PACKET};

fn decoded(encoded: &[u8]) -> Vec<u8> {
    let mut decoder = RnsSerialDecoder::<WDCL_MAX_CHUNK>::new();
    let mut frames = Vec::new();
    decoder.feed_slice(encoded, |frame| frames.push(frame.to_vec()));
    assert_eq!(frames.len(), 1);
    frames.remove(0)
}

fn from_hex(value: &str) -> Vec<u8> {
    value
        .as_bytes()
        .chunks_exact(2)
        .map(|pair| {
            let high = (pair[0] as char).to_digit(16).unwrap() as u8;
            let low = (pair[1] as char).to_digit(16).unwrap() as u8;
            (high << 4) | low
        })
        .collect()
}

#[test]
fn discovery_broadcast_contains_the_host_switch_id() {
    let identity = WeaveHostIdentity::from_signing_secret([0x11; 32]);
    let mut output = [0u8; 64];
    let written = super::encode_discovery(&identity, &mut output).unwrap();
    let frame = decoded(&output[..written]);
    assert_eq!(&frame[..SwitchId::LEN], BROADCAST_SWITCH.as_bytes());
    assert_eq!(frame[SwitchId::LEN], super::TYPE_DISCOVER);
    assert_eq!(&frame[SwitchId::LEN + 1..], identity.switch_id().as_bytes());
    assert_eq!(
        &output[..written],
        from_hex("7effffffff00c97787377e").as_slice()
    );
}

#[test]
fn handshake_matches_the_rns_1_4_2_python_oracle() {
    let identity = WeaveHostIdentity::from_signing_secret([0x11; 32]);
    let remote = SwitchId::new([0x34, 0x55, 0xa4, 0xf0]);
    let mut output = [0u8; 256];
    let written = super::encode_handshake(&identity, remote, &mut output).unwrap();
    assert_eq!(
        &output[..written],
        from_hex(
            "7e3455a4f001d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c977873714b47546507ad58f938b7bfbcc6c077aab9fb0bd0c62eb5b1d89f6f9c4e34735f9b40e7d5e4cdfbd0778c5d5ca766fdc7703c21053c77c21be3dae1c569f12760b7e"
        )
        .as_slice()
    );
}

#[test]
fn discovery_response_verification_matches_the_rns_1_4_2_python_oracle() {
    let frame = from_hex(
        "c977873700a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0d6d59bd10d8d43ad27e2c66ffe4be08f4c15408d75698ec87dfcf7e68430efac79a655716f2ea93a60cdf1cc0d6bc2d74310bf067c52d1b6405bdff9f9998a08",
    );
    assert_eq!(
        super::decode_device_frame(&frame, SwitchId::new([0xc9, 0x77, 0x87, 0x37])),
        Ok(DeviceEvent::Discovered {
            switch_id: SwitchId::new([0x34, 0x55, 0xa4, 0xf0]),
            signing_public_key: crate::crypto::Ed25519PublicKey([
                0xa0, 0x9a, 0xa5, 0xf4, 0x7a, 0x67, 0x59, 0x80, 0x2f, 0xf9, 0x55, 0xf8, 0xdc, 0x2d,
                0x2a, 0x14, 0xa5, 0xc9, 0x9d, 0x23, 0xbe, 0x97, 0xf8, 0x64, 0x12, 0x7f, 0xf9, 0x38,
                0x34, 0x55, 0xa4, 0xf0,
            ]),
        })
    );
    let device = WeaveHostIdentity::from_signing_secret([0x22; 32]);
    let mut encoded = [0u8; 256];
    let written = super::encode_discovery_response(
        &device,
        SwitchId::new([0xc9, 0x77, 0x87, 0x37]),
        &mut encoded,
    )
    .unwrap();
    assert_eq!(decoded(&encoded[..written]), frame);
}

#[test]
fn discovery_rejects_a_signature_for_a_different_host_switch() {
    let device = WeaveHostIdentity::from_signing_secret([0x22; 32]);
    let mut encoded = [0u8; 256];
    let written = super::encode_discovery_response(
        &device,
        SwitchId::new([0xc9, 0x77, 0x87, 0x37]),
        &mut encoded,
    )
    .unwrap();
    let mut frame = decoded(&encoded[..written]);
    frame[0] ^= 1;

    assert_eq!(
        super::decode_device_frame(&frame, SwitchId::new([0xc9, 0x77, 0x87, 0x37])),
        Err(super::DecodeError::InvalidDiscoverySignature)
    );
}

#[test]
fn endpoint_command_carries_the_typed_destination_and_packet() {
    let switch = SwitchId::new([1, 2, 3, 4]);
    let endpoint = EndpointId::new([5, 6, 7, 8, 9, 10, 11, 12]);
    let mut raw = [0u8; 64];
    let mut output = [0u8; 128];
    let written =
        super::encode_endpoint_packet(switch, endpoint, b"packet", &mut raw, &mut output).unwrap();
    let frame = decoded(&output[..written]);
    assert_eq!(&frame[..4], switch.as_bytes());
    assert_eq!(frame[4], super::TYPE_COMMAND);
    assert_eq!(&frame[5..7], &super::COMMAND_ENDPOINT_PACKET.to_be_bytes());
    assert_eq!(&frame[7..15], endpoint.as_bytes());
    assert_eq!(&frame[15..], b"packet");
}

#[test]
fn endpoint_command_retains_ifac_headroom_beyond_the_fixed_mtu() {
    let payload = vec![0x42; WEAVE_MAX_WIRE_PACKET];
    let mut raw = vec![0u8; SwitchId::LEN + 1 + 2 + EndpointId::LEN + payload.len()];
    let mut output = vec![0u8; rns_serial_framing::max_encoded_len(raw.len())];
    let written = super::encode_endpoint_packet(
        SwitchId::new([1, 2, 3, 4]),
        EndpointId::new([5, 6, 7, 8, 9, 10, 11, 12]),
        &payload,
        &mut raw,
        &mut output,
    )
    .unwrap();
    let frame = decoded(&output[..written]);

    assert_eq!(&frame[15..], payload);
}

#[test]
fn endpoint_packet_exposes_source_and_payload_without_copying() {
    let source = EndpointId::new([0x22; 8]);
    let mut frame = Vec::from([0x11, 0x12, 0x13, 0x14, super::TYPE_ENDPOINT_PACKET]);
    frame.extend_from_slice(b"packet");
    frame.extend_from_slice(source.as_bytes());
    assert_eq!(
        super::decode_device_frame(&frame, SwitchId::new([0x11, 0x12, 0x13, 0x14])),
        Ok(DeviceEvent::EndpointPacket {
            source,
            payload: b"packet",
        })
    );
}

#[test]
fn endpoint_packet_for_another_switch_is_not_delivered() {
    let source = EndpointId::new([0x22; 8]);
    let mut frame = Vec::from([0x11, 0x12, 0x13, 0x14, super::TYPE_ENDPOINT_PACKET]);
    frame.extend_from_slice(b"packet");
    frame.extend_from_slice(source.as_bytes());

    assert_eq!(
        super::decode_device_frame(&frame, SwitchId::new([0x21, 0x22, 0x23, 0x24])),
        Ok(DeviceEvent::Ignored)
    );
}

#[test]
fn multipath_duplicate_expires_at_the_reference_deadline() {
    let mut deduplicator = MultipathDeduplicator::new();
    assert!(deduplicator.accepts(b"packet", 1_000));
    assert!(!deduplicator.accepts(b"packet", 1_749));
    assert!(deduplicator.accepts(b"packet", 1_750));
    assert!(deduplicator.accepts(b"different", 1_750));
}