rscamper 0.2.2

Rust interface to scamper network measurement tool
Documentation
// rscamper - Rust wrapper for scamper instance connections
//
// Copyright (C) 2026 Dimitrios Giakatos
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3.

use std::ffi::CStr;

use crate::ffi::libscamperctrl::{self, ScamperInstT, ScamperTaskT};
use crate::ffi::scamper_file::{self, ScamperFileT, ScamperFileReadbufT};
use crate::vp::ScamperVp;

/// Per-instance state stored as the inst param (accessed by the ctrl callback).
pub(crate) struct InstData {
    pub(crate) c_f: *mut ScamperFileT,
    pub(crate) c_rb: *mut ScamperFileReadbufT,
    pub(crate) eof: bool,
    pub(crate) queued: usize,
    pub(crate) tasks: Vec<*mut ScamperTaskT>,
}

/// A connection to a single scamper instance.
///
/// Created via `ScamperCtrl::add_unix`, `add_inet`, or `add_remote`.
/// Provides methods for querying the instance's state.
/// Issue measurements via the `ScamperCtrl::do_*` methods, passing a reference to this.
pub struct ScamperInst {
    pub(crate) c: *mut ScamperInstT,
    pub(crate) data: *mut InstData,
}

impl ScamperInst {
    /// Create a new ScamperInst wrapping a raw pointer.
    /// Sets up the readbuf and null file for warts parsing.
    pub(crate) unsafe fn from_ptr(c: *mut ScamperInstT) -> Self {
        let c_rb = unsafe { scamper_file::scamper_file_readbuf_alloc() };
        let kind = b"warts\0";
        let c_f = unsafe { scamper_file::scamper_file_opennull(
            b'r' as i8,
            kind.as_ptr() as *const libc::c_char,
        ) };
        unsafe { scamper_file::scamper_file_setreadfunc(
            c_f,
            c_rb as *mut libc::c_void,
            scamper_file::scamper_file_readbuf_read,
        ) };

        let data = Box::into_raw(Box::new(InstData {
            c_f,
            c_rb,
            eof: false,
            queued: 0,
            tasks: Vec::new(),
        }));

        unsafe { libscamperctrl::scamper_inst_param_set(c, data as *mut libc::c_void) };

        ScamperInst { c, data }
    }

    /// The name of this instance (path or address).
    pub fn name(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_inst_name_get(self.c) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned())
        }
    }

    /// True if this is a unix-domain socket instance.
    pub fn is_unix(&self) -> bool {
        unsafe { libscamperctrl::scamper_inst_is_unix(self.c) != 0 }
    }

    /// True if this is an inet (TCP) instance.
    pub fn is_inet(&self) -> bool {
        unsafe { libscamperctrl::scamper_inst_is_inet(self.c) != 0 }
    }

    /// True if this is a remote instance.
    pub fn is_remote(&self) -> bool {
        unsafe { libscamperctrl::scamper_inst_is_remote(self.c) != 0 }
    }

    /// True if this is a mux VP instance.
    pub fn is_muxvp(&self) -> bool {
        unsafe { libscamperctrl::scamper_inst_is_muxvp(self.c) != 0 }
    }

    /// True if the instance has signalled EOF (disconnected).
    pub fn is_eof(&self) -> bool {
        unsafe { (*self.data).eof }
    }

    /// The number of outstanding (unresolved) tasks for this instance.
    pub fn taskc(&self) -> usize {
        unsafe { (*self.data).tasks.len() }
    }

    /// The number of completed results queued but not yet consumed.
    pub fn resultc(&self) -> usize {
        unsafe { (*self.data).queued }
    }

    /// The vantage point associated with this instance, if any.
    pub fn vp(&self) -> Option<ScamperVp> {
        let ptr = unsafe { libscamperctrl::scamper_inst_vp_get(self.c) };
        unsafe { ScamperVp::from_ptr(ptr) }
    }

    /// The country code of this instance's VP, if known.
    pub fn cc(&self) -> Option<String> { self.vp()?.cc() }

    /// The state code of this instance's VP, if known.
    pub fn st(&self) -> Option<String> { self.vp()?.st() }

    /// The place name of this instance's VP, if known.
    pub fn place(&self) -> Option<String> { self.vp()?.place() }

    /// The short name of this instance's VP, if known.
    pub fn shortname(&self) -> Option<String> { self.vp()?.shortname() }

    /// The IPv4 address of this instance's VP, if known.
    pub fn ipv4(&self) -> Option<String> { self.vp()?.ipv4() }

    /// The IPv4 ASN of this instance's VP, if known.
    pub fn asn4(&self) -> Option<String> { self.vp()?.asn4() }

    /// The location (lat, lon) of this instance's VP, if known.
    pub fn loc(&self) -> Option<(f64, f64)> { self.vp()?.loc() }

    /// The IATA code of this instance's VP, if known.
    pub fn iata(&self) -> Option<String> { self.vp()?.iata() }

    /// Signal that no further measurements will be issued on this instance.
    ///
    /// **Must** be called after all `do_*` scheduling calls for this instance
    /// and before collecting results. Without it, [`ScamperCtrl::responses`]
    /// and [`ScamperCtrl::poll`] will block indefinitely because the event
    /// loop keeps waiting for commands that will never arrive.
    ///
    /// After calling `done()` you cannot submit new measurements on this
    /// instance. Create a new instance via `add_inet` / `add_unix` /
    /// `add_remote` if you need a second batch.
    ///
    /// # Example
    ///
    /// ```no_run
    /// ctrl.do_ping(&inst, "1.1.1.1", /* … */)?;
    /// ctrl.do_trace(&inst, "8.8.8.8", /* … */)?;
    /// inst.done(); // must come after all do_* calls for this inst
    ///
    /// for item in ctrl.responses(None) { /* … */ }
    /// ```
    pub fn done(&self) {
        unsafe { libscamperctrl::scamper_inst_done(self.c) };
    }
}

impl Drop for ScamperInst {
    fn drop(&mut self) {
        unsafe {
            let data = Box::from_raw(self.data);
            scamper_file::scamper_file_close(data.c_f);
            scamper_file::scamper_file_readbuf_free(data.c_rb);
            // data (and its tasks vec) is dropped here
            drop(data);
            libscamperctrl::scamper_inst_free(self.c);
        }
    }
}

impl PartialEq for ScamperInst {
    fn eq(&self, other: &Self) -> bool { self.c == other.c }
}

impl Eq for ScamperInst {}

impl std::hash::Hash for ScamperInst {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        (self.c as usize).hash(state);
    }
}

impl std::fmt::Display for ScamperInst {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.name() {
            Some(n) => write!(f, "{}", n),
            None => write!(f, "<ScamperInst {:p}>", self.c),
        }
    }
}

unsafe impl Send for ScamperInst {}
unsafe impl Sync for ScamperInst {}