i2pd-sys 0.0.4

Raw FFI bindings to a minimal C shim over libi2pd (PurpleI2P/i2pd).
Documentation
//! Exercises destination/key lifecycle against a real (in-process) i2pd router, without ever
//! building a tunnel to a remote peer -- unlike `tests/roundtrip.rs`, nothing here waits on live
//! I2P network state, so this runs as an ordinary (non-`#[ignore]`d) ~sub-second test.
//!
//! Deliberately a *single* `#[test]` function covering multiple scenarios sequentially, rather
//! than several independent ones: `i2pd_init`/`i2pd_terminate` set up and tear down process-wide
//! router-context/crypto globals in `libi2pd`, which are not documented as safe to initialize
//! concurrently from multiple threads -- and `cargo test` runs separate `#[test]` functions on
//! separate threads within the same process by default. One combined test with one init/terminate
//! cycle sidesteps that hazard entirely. (Argument-validation tests that never reach i2pd's
//! globals live in `tests/null_safety.rs` instead, where running them independently and in
//! parallel is actually safe.)
#![allow(unsafe_code, clippy::missing_safety_doc)]

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

// libi2pd/Identity.h's stable protocol-level enum values -- see shim.h's i2pd_generate_keys doc
// comment. EdDSA-Ed25519 signatures + ECIES-X25519-AEAD encryption is i2pd's modern default pair
// for newly created destinations.
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());
        i2pd_start();

        // --- transient destination: created, addressable, cleanly destroyed ---
        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);

        // --- persistent destination: key generation is deterministic and round-trips ---
        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);

        // --- two independent key-generation calls must not collide ---
        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);
        assert_ne!(keys_bytes, keys2_bytes, "two independent key generations produced identical keys");

        i2pd_stop();
        i2pd_terminate();
    }
}