i2pd-sys 0.0.6

Raw FFI bindings to a minimal C shim over libi2pd (PurpleI2P/i2pd)
Documentation
//! Destination/key lifecycle against a real in-process router, without building a tunnel to a
//! remote peer -- unlike `tests/roundtrip.rs`, nothing here waits on live network state.
//!
//! One `#[test]` covering every scenario sequentially, not several independent ones:
//! `i2pd_init`/`i2pd_terminate` drive process-wide router-context/crypto globals that libi2pd does
//! not document as safe to initialize concurrently, and `cargo test` runs `#[test]` functions on
//! separate threads in one process. Argument validation that never reaches those globals lives in
//! `tests/null_safety.rs`, where parallelism is safe.
#![allow(unsafe_code, clippy::missing_safety_doc)]

use i2pd_sys::*;
use std::ffi::CString;

// libi2pd/Identity.h protocol-level enum values -- see shim.h's i2pd_generate_keys.
const SIG_TYPE_EDDSA_ED25519: i32 = 7;
const CRYPTO_TYPE_ECIES_X25519_AEAD: i32 = 4;

#[test]
fn local_destination_and_key_lifecycle() {
    unsafe {
        let app_name = CString::new("tachyon-i2pd-local-lifecycle-test").unwrap();
        i2pd_init(app_name.as_ptr());
        // A second init must be inert, not a re-run of libi2pd's global construction underneath
        // a live router.
        i2pd_init(app_name.as_ptr());
        i2pd_start();
        i2pd_start();

        let transient = i2pd_create_transient_destination();
        assert!(!transient.is_null(), "failed to create transient destination");

        let transient_addr = i2pd_destination_b32_address(transient);
        assert!(!transient_addr.is_null());
        let transient_addr_str = std::ffi::CStr::from_ptr(transient_addr).to_str().unwrap().to_owned();
        assert!(transient_addr_str.ends_with(".b32.i2p"), "unexpected address format: {transient_addr_str}");
        assert_eq!(
            transient_addr_str.len(),
            52 + ".b32.i2p".len(),
            "b32 address has unexpected length: {transient_addr_str}"
        );
        i2pd_free_string(transient_addr);

        let mut transient_hash = [0u8; 32];
        assert_eq!(i2pd_destination_ident_hash(transient, transient_hash.as_mut_ptr()), 1);
        assert_ne!(transient_hash, [0u8; 32], "ident hash should not be all-zero");

        i2pd_destroy_destination(transient);

        let mut keys_buf: *mut u8 = std::ptr::null_mut();
        let mut keys_len: usize = 0;
        assert_eq!(
            i2pd_generate_keys(
                SIG_TYPE_EDDSA_ED25519,
                CRYPTO_TYPE_ECIES_X25519_AEAD,
                &mut keys_buf,
                &mut keys_len,
            ),
            1,
            "key generation failed"
        );
        assert!(!keys_buf.is_null());
        assert!(keys_len > 0);
        let keys_bytes = std::slice::from_raw_parts(keys_buf, keys_len).to_vec();

        let dest_a =
            i2pd_create_persistent_destination(keys_bytes.as_ptr(), keys_bytes.len(), 1, std::ptr::null());
        assert!(!dest_a.is_null(), "failed to create persistent destination from generated keys");
        let addr_a_ptr = i2pd_destination_b32_address(dest_a);
        assert!(!addr_a_ptr.is_null());
        let addr_a = std::ffi::CStr::from_ptr(addr_a_ptr).to_str().unwrap().to_owned();
        i2pd_free_string(addr_a_ptr);
        i2pd_destroy_destination(dest_a);

        // Recreating a destination from the *same* serialized keys must yield the identical
        // address -- this is the whole point of persistent (as opposed to transient) keys.
        let dest_b =
            i2pd_create_persistent_destination(keys_bytes.as_ptr(), keys_bytes.len(), 1, std::ptr::null());
        assert!(!dest_b.is_null());
        let addr_b_ptr = i2pd_destination_b32_address(dest_b);
        let addr_b = std::ffi::CStr::from_ptr(addr_b_ptr).to_str().unwrap().to_owned();
        i2pd_free_string(addr_b_ptr);
        i2pd_destroy_destination(dest_b);

        assert_eq!(addr_a, addr_b, "same keys must produce the same b32 address");

        i2pd_free_buffer(keys_buf, keys_len);

        let mut buf2: *mut u8 = std::ptr::null_mut();
        let mut len2: usize = 0;
        assert_eq!(
            i2pd_generate_keys(SIG_TYPE_EDDSA_ED25519, CRYPTO_TYPE_ECIES_X25519_AEAD, &mut buf2, &mut len2),
            1
        );
        let keys2_bytes = std::slice::from_raw_parts(buf2, len2).to_vec();
        i2pd_free_buffer(buf2, len2);
        assert_ne!(keys_bytes, keys2_bytes, "two independent key generations produced identical keys");

        i2pd_stop();
        // Likewise for repeated teardown.
        i2pd_stop();
        i2pd_terminate();
        i2pd_terminate();

        // shim.h documents re-init after terminate as allowed.
        i2pd_init(app_name.as_ptr());
        i2pd_start();
        let restarted = i2pd_create_transient_destination();
        assert!(!restarted.is_null(), "router did not come back up after terminate + re-init");
        i2pd_destroy_destination(restarted);

        // Deliberately no i2pd_stop() here: terminate has to stop the running router itself.
        i2pd_terminate();

        // A third cycle is what actually pins that down: had the bare terminate above left the
        // started-flag set, this i2pd_start would be a silent no-op.
        i2pd_init(app_name.as_ptr());
        i2pd_start();
        let after_bare_terminate = i2pd_create_transient_destination();
        assert!(
            !after_bare_terminate.is_null(),
            "terminate without a preceding stop latched the router shut"
        );
        i2pd_destroy_destination(after_bare_terminate);
        i2pd_stop();
        i2pd_terminate();
    }
}