i2pd-sys 0.0.4

Raw FFI bindings to a minimal C shim over libi2pd (PurpleI2P/i2pd).
Documentation
//! Round-trip test: two transient destinations, created from the same in-process i2pd router,
//! exchange bytes over a real stream. This is deliberately the *only* test in this crate that
//! touches the live I2P network, hence `#[ignore]`d by default: real tunnel building over the
//! live network is required even for two destinations that happen to share a process, so this
//! can take several minutes and is not something to run on every `cargo test`.
//!
//! Run explicitly with:
//! ```sh
//! cargo test -p i2pd-sys --test roundtrip -- --ignored --nocapture
//! ```
#![allow(unsafe_code, clippy::missing_safety_doc)]

use i2pd_sys::*;
use std::ffi::CString;
use std::os::raw::c_void;
use std::sync::mpsc;
use std::time::Duration;

/// Wraps a raw stream handle so it can cross the mpsc channel from i2pd's acceptor callback
/// (invoked on i2pd's own internal thread) to the test's main thread.
struct SendableStream(*mut I2pdStream);
unsafe impl Send for SendableStream {}

unsafe extern "C" fn on_accept(ctx: *mut c_void, stream: *mut I2pdStream) {
    let tx = unsafe { &*(ctx as *const mpsc::Sender<SendableStream>) };
    let _ = tx.send(SendableStream(stream));
}

#[test]
#[ignore = "needs live I2P network egress and can take several minutes to build real tunnels"]
fn two_transient_destinations_exchange_bytes() {
    unsafe {
        let app_name = CString::new("tachyon-i2pd-roundtrip-test").unwrap();
        i2pd_init(app_name.as_ptr());
        i2pd_start();

        let dest_a = i2pd_create_transient_destination();
        assert!(!dest_a.is_null(), "failed to create destination A");
        let dest_b = i2pd_create_transient_destination();
        assert!(!dest_b.is_null(), "failed to create destination B");

        let (tx, rx) = mpsc::channel::<SendableStream>();
        i2pd_accept_stream(dest_b, Some(on_accept), (&tx as *const mpsc::Sender<_>) as *mut c_void);

        let mut b_hash = [0u8; 32];
        assert_eq!(i2pd_destination_ident_hash(dest_b, b_hash.as_mut_ptr()), 1);

        eprintln!("waiting for destination A to build a stream to destination B (up to 180s)...");
        let stream_a = i2pd_create_stream(dest_a, b_hash.as_ptr(), 180);
        assert!(!stream_a.is_null(), "destination A failed to connect to destination B within 180s");

        eprintln!("stream connected from A's side, waiting for B's acceptor to fire...");
        let SendableStream(stream_b) = rx
            .recv_timeout(Duration::from_secs(30))
            .expect("destination B's acceptor did not fire within 30s of A connecting");
        assert!(!stream_b.is_null());

        let payload = b"hello over a real i2p stream";
        let sent = i2pd_stream_send(stream_a, payload.as_ptr(), payload.len());
        assert_eq!(sent, payload.len() as std::os::raw::c_long, "short/failed send");

        let mut buf = [0u8; 64];
        let received = i2pd_stream_receive(stream_b, buf.as_mut_ptr(), buf.len(), 30);
        assert_eq!(received, payload.len() as std::os::raw::c_long, "short/failed receive");
        assert_eq!(&buf[..received as usize], payload, "round-tripped bytes did not match");

        i2pd_stream_close(stream_a);
        i2pd_stream_close(stream_b);
        i2pd_destroy_stream(stream_a);
        i2pd_destroy_stream(stream_b);
        i2pd_destroy_destination(dest_a);
        i2pd_destroy_destination(dest_b);

        i2pd_stop();
        i2pd_terminate();
    }
}