prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use super::*;

const PYTHON_BACKBONE: &str = "8b00b14261636b626f6e65496e7465726661636501c3ccfec41000112233445566778899aabbccddeeffccffaf5075626c6963204261636b626f6e6503cb402900000000000004cbc04120000000000005cb405ec0000000000002ae726f757465722e6578616d706c6506cd109207a46d65736808a6736563726574";

fn backbone() -> DiscoveryAdvertisement {
    DiscoveryAdvertisement {
        interface_type: AdvertisedInterfaceType::Backbone,
        transport: AdvertisedTransport::Enabled(TransportId::new([
            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
            0xee, 0xff,
        ])),
        name: Some(String::from("Public Backbone")),
        location: GeographicLocation {
            latitude: Some(12.5),
            longitude: Some(-34.25),
            height: Some(123.0),
        },
        details: AdvertisementDetails::Reachable {
            host: String::from("router.example"),
            port: 4242,
        },
        published_ifac: Some(PublishedIfac {
            network_name: Some(String::from("mesh")),
            passphrase: Some(String::from("secret")),
        }),
    }
}

fn bytes_from_hex(hex: &str) -> Vec<u8> {
    hex.as_bytes()
        .chunks_exact(2)
        .map(|pair| u8::from_str_radix(core::str::from_utf8(pair).unwrap(), 16).unwrap())
        .collect()
}

fn encoded_backbone() -> Vec<u8> {
    match encode_advertisement(&backbone()) {
        Ok(encoded) => encoded,
        Err(error) => panic!("unexpected encode error: {error}"),
    }
}

#[test]
fn python_rns_1_4_2_advertisement_is_byte_identical_and_decodes() {
    let packed = bytes_from_hex(PYTHON_BACKBONE);
    assert_eq!(encode_advertisement(&backbone()), Ok(packed.clone()));
    assert_eq!(decode_advertisement(&packed), Ok(backbone()));
}

#[test]
fn every_supported_detail_shape_round_trips() {
    let details = [
        (
            AdvertisedInterfaceType::TcpClient,
            AdvertisementDetails::None,
        ),
        (
            AdvertisedInterfaceType::I2p,
            AdvertisementDetails::I2p {
                address: String::from("peer.b32.i2p"),
            },
        ),
        (
            AdvertisedInterfaceType::RNode,
            AdvertisementDetails::RNode {
                frequency_hz: 867_200_000,
                bandwidth_hz: 125_000,
                spreading_factor: 9,
                coding_rate: 5,
            },
        ),
        (
            AdvertisedInterfaceType::Weave,
            AdvertisementDetails::Weave {
                frequency_hz: 915_000_000,
                bandwidth_hz: 250_000,
                channel: 3,
                modulation: String::from("GFSK"),
            },
        ),
        (
            AdvertisedInterfaceType::Kiss,
            AdvertisementDetails::Kiss {
                frequency_hz: 144_800_000,
                bandwidth_hz: 12_500,
                modulation: String::from("AFSK"),
            },
        ),
    ];
    for (interface_type, details) in details {
        let advertisement = DiscoveryAdvertisement {
            interface_type,
            transport: AdvertisedTransport::Disabled(TransportId::new([0x33; 16])),
            name: None,
            location: GeographicLocation::UNKNOWN,
            details,
            published_ifac: None,
        };
        let encoded = match encode_advertisement(&advertisement) {
            Ok(encoded) => encoded,
            Err(error) => panic!("unexpected encode error: {error}"),
        };
        assert_eq!(decode_advertisement(&encoded), Ok(advertisement));
    }
}

#[test]
fn mismatched_details_and_duplicate_known_fields_are_rejected() {
    let advertisement = DiscoveryAdvertisement {
        interface_type: AdvertisedInterfaceType::Backbone,
        transport: AdvertisedTransport::Disabled(TransportId::new([0x33; 16])),
        name: None,
        location: GeographicLocation::UNKNOWN,
        details: AdvertisementDetails::None,
        published_ifac: None,
    };
    assert_eq!(
        encode_advertisement(&advertisement),
        Err(DiscoveryEncodeError::DetailsDoNotMatchInterfaceType),
    );

    let mut duplicate = encoded_backbone();
    duplicate[0] = 0x8c;
    duplicate.push(INTERFACE_TYPE as u8);
    duplicate.push(0xb1);
    duplicate.extend_from_slice(b"BackboneInterface");
    assert_eq!(
        decode_advertisement(&duplicate),
        Err(DiscoveryDecodeError::DuplicateField(
            DiscoveryField::InterfaceType,
        )),
    );
}

#[test]
fn required_bool_is_not_coerced_from_an_integer() {
    let mut encoded = encoded_backbone();
    let transport_value = encoded
        .windows(2)
        .position(|pair| pair == [TRANSPORT as u8, 0xc3]);
    let Some(transport_value) = transport_value else {
        panic!("transport field not found");
    };
    encoded[transport_value + 1] = 1;
    assert_eq!(
        decode_advertisement(&encoded),
        Err(DiscoveryDecodeError::InvalidField(
            DiscoveryField::Transport,
        )),
    );
}

#[test]
fn independently_published_ifac_fields_match_rns_receiver_tolerance() {
    let mut advertisement = backbone();
    advertisement.published_ifac = None;
    let mut encoded = match encode_advertisement(&advertisement) {
        Ok(encoded) => encoded,
        Err(error) => panic!("unexpected encode error: {error}"),
    };
    encoded[0] += 1;
    encoded.extend_from_slice(&[IFAC_NETNAME as u8, 0xa4, b'm', b'e', b's', b'h']);
    advertisement.published_ifac = Some(PublishedIfac {
        network_name: Some(String::from("mesh")),
        passphrase: None,
    });
    assert_eq!(decode_advertisement(&encoded), Ok(advertisement));
}

#[test]
fn trailing_data_and_unknown_nested_values_are_handled_explicitly() {
    let mut trailing = encoded_backbone();
    trailing.push(0);
    assert_eq!(
        decode_advertisement(&trailing),
        Err(DiscoveryDecodeError::TrailingData),
    );

    let mut extended = encoded_backbone();
    extended[0] = 0x8d;
    extended.extend_from_slice(&[0x20, 0x91, 0x81, 0xa1, b'x', 0xc3]);
    extended.extend_from_slice(&[0xd0, 0xff, 0xc0]);
    assert_eq!(decode_advertisement(&extended), Ok(backbone()));
}

#[test]
fn plaintext_and_encrypted_envelopes_preserve_their_boundaries() {
    let packed = encoded_backbone();
    let stamp = [0x5a; STAMP_SIZE];
    let plaintext = encode_plaintext_envelope(&packed, &stamp);
    assert_eq!(
        decode_envelope(&plaintext),
        Ok(DiscoveryEnvelope {
            signed: false,
            body: DiscoveryEnvelopeBody::Plaintext {
                packed_advertisement: &packed,
                stamp: &stamp,
            },
        }),
    );

    let ciphertext = [0xa5; 96];
    let encrypted = encode_encrypted_envelope(&ciphertext);
    assert_eq!(
        decode_envelope(&encrypted),
        Ok(DiscoveryEnvelope {
            signed: false,
            body: DiscoveryEnvelopeBody::Encrypted {
                ciphertext: &ciphertext,
            },
        }),
    );
}

#[test]
fn envelope_rejects_missing_plaintext_without_rejecting_opaque_encrypted_data() {
    assert_eq!(
        decode_envelope(&[]),
        Err(DiscoveryEnvelopeError::MissingFlags),
    );
    assert_eq!(
        decode_envelope(&[0]),
        Err(DiscoveryEnvelopeError::MissingPlaintextOrStamp),
    );
    assert_eq!(
        decode_envelope(&[FLAG_ENCRYPTED]),
        Ok(DiscoveryEnvelope {
            signed: false,
            body: DiscoveryEnvelopeBody::Encrypted { ciphertext: &[] },
        }),
    );
}