rscamper 0.2.2

Rust interface to scamper network measurement tool
Documentation
// rscamper - Rust wrapper for scamper DNS query results
//
// 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 std::time::{Duration, UNIX_EPOCH, SystemTime};

use crate::addr::ScamperAddr;
use crate::list::{ScamperList, ScamperCycle};
use crate::ffi::scamper_host::{self, ScamperHostT, ScamperHostQueryT, ScamperHostRrT,
    ScamperHostRrSoaT, ScamperHostRrMxT, ScamperHostRrTxtT,
    ScamperHostRrOptT, ScamperHostRrOptElemT, ScamperHostRrSvcbT, ScamperHostRrSvcbParamT,
    SCAMPER_HOST_STOP_NONE, SCAMPER_HOST_STOP_DONE, SCAMPER_HOST_STOP_TIMEOUT,
    SCAMPER_HOST_STOP_HALTED, SCAMPER_HOST_STOP_ERROR};

fn timeval_to_duration(tv: *const libc::timeval) -> Option<Duration> {
    if tv.is_null() { return None; }
    let tv = unsafe { &*tv };
    Some(Duration::new(tv.tv_sec as u64, tv.tv_usec as u32 * 1000))
}

fn timeval_to_systemtime(tv: *const libc::timeval) -> Option<SystemTime> {
    timeval_to_duration(tv).map(|d| UNIX_EPOCH + d)
}

/// Stop reasons for a DNS host measurement.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostStopReason {
    None, Done, Timeout, Halted, Error, Unknown(u8),
}

impl From<u8> for HostStopReason {
    fn from(v: u8) -> Self {
        match v {
            SCAMPER_HOST_STOP_NONE    => Self::None,
            SCAMPER_HOST_STOP_DONE    => Self::Done,
            SCAMPER_HOST_STOP_TIMEOUT => Self::Timeout,
            SCAMPER_HOST_STOP_HALTED  => Self::Halted,
            SCAMPER_HOST_STOP_ERROR   => Self::Error,
            other                     => Self::Unknown(other),
        }
    }
}

/// A DNS SOA record.
pub struct ScamperHostSoa { inner: *mut ScamperHostRrSoaT }
impl ScamperHostSoa {
    pub fn mname(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_rr_soa_mname_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }
    pub fn rname(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_rr_soa_rname_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }
    pub fn serial(&self) -> u32 { unsafe { scamper_host::scamper_host_rr_soa_serial_get(self.inner) } }
    pub fn refresh(&self) -> u32 { unsafe { scamper_host::scamper_host_rr_soa_refresh_get(self.inner) } }
    pub fn retry(&self) -> u32 { unsafe { scamper_host::scamper_host_rr_soa_retry_get(self.inner) } }
    pub fn expire(&self) -> u32 { unsafe { scamper_host::scamper_host_rr_soa_expire_get(self.inner) } }
    pub fn minimum(&self) -> u32 { unsafe { scamper_host::scamper_host_rr_soa_minimum_get(self.inner) } }
}
impl Drop for ScamperHostSoa {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_soa_free(self.inner) }; }
}
unsafe impl Send for ScamperHostSoa {}

/// A DNS MX record.
pub struct ScamperHostMx { inner: *mut ScamperHostRrMxT }
impl ScamperHostMx {
    pub fn preference(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_mx_preference_get(self.inner) } }
    pub fn exchange(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_rr_mx_exchange_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }
}
impl Drop for ScamperHostMx {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_mx_free(self.inner) }; }
}
unsafe impl Send for ScamperHostMx {}

/// A DNS TXT record.
pub struct ScamperHostTxt { inner: *mut ScamperHostRrTxtT }
impl ScamperHostTxt {
    pub fn strc(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_txt_strc_get(self.inner) } }
    pub fn str_val(&self, i: u16) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_rr_txt_str_get(self.inner, i) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }
}
impl Drop for ScamperHostTxt {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_txt_free(self.inner) }; }
}
unsafe impl Send for ScamperHostTxt {}

/// An element in a DNS OPT record.
pub struct ScamperHostOptElem { inner: *mut ScamperHostRrOptElemT }
impl ScamperHostOptElem {
    pub fn code(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_opt_elem_code_get(self.inner) } }
    pub fn len(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_opt_elem_len_get(self.inner) } }
    pub fn data(&self) -> &[u8] {
        let len = self.len() as usize;
        let ptr = unsafe { scamper_host::scamper_host_rr_opt_elem_data_get(self.inner) };
        if ptr.is_null() || len == 0 { &[] } else { unsafe { std::slice::from_raw_parts(ptr, len) } }
    }
}
impl Drop for ScamperHostOptElem {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_opt_elem_free(self.inner) }; }
}
unsafe impl Send for ScamperHostOptElem {}

/// A DNS OPT record.
pub struct ScamperHostOpt { inner: *mut ScamperHostRrOptT }
impl ScamperHostOpt {
    pub fn elemc(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_opt_elemc_get(self.inner) } }
    pub fn elem(&self, i: u16) -> Option<ScamperHostOptElem> {
        let ptr = unsafe { scamper_host::scamper_host_rr_opt_elem_get(self.inner, i) };
        if ptr.is_null() { None } else {
            let ptr = unsafe { scamper_host::scamper_host_rr_opt_elem_use(ptr) };
            Some(ScamperHostOptElem { inner: ptr })
        }
    }
}
impl Drop for ScamperHostOpt {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_opt_free(self.inner) }; }
}
unsafe impl Send for ScamperHostOpt {}

/// A parameter in a DNS SVCB record.
pub struct ScamperHostSvcbParam { inner: *mut ScamperHostRrSvcbParamT }
impl ScamperHostSvcbParam {
    pub fn key(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_svcb_param_key_get(self.inner) } }
    pub fn len(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_svcb_param_len_get(self.inner) } }
    pub fn val(&self) -> &[u8] {
        let len = self.len() as usize;
        let ptr = unsafe { scamper_host::scamper_host_rr_svcb_param_val_get(self.inner) };
        if ptr.is_null() || len == 0 { &[] } else { unsafe { std::slice::from_raw_parts(ptr, len) } }
    }
}
impl Drop for ScamperHostSvcbParam {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_svcb_param_free(self.inner) }; }
}
unsafe impl Send for ScamperHostSvcbParam {}

/// A DNS SVCB record.
pub struct ScamperHostSvcb { inner: *mut ScamperHostRrSvcbT }
impl ScamperHostSvcb {
    pub fn target(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_rr_svcb_target_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }
    pub fn priority(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_svcb_priority_get(self.inner) } }
    pub fn paramc(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_svcb_paramc_get(self.inner) } }
    pub fn param(&self, i: u16) -> Option<ScamperHostSvcbParam> {
        let ptr = unsafe { scamper_host::scamper_host_rr_svcb_param_get(self.inner, i) };
        if ptr.is_null() { None } else {
            let ptr = unsafe { scamper_host::scamper_host_rr_svcb_param_use(ptr) };
            Some(ScamperHostSvcbParam { inner: ptr })
        }
    }
}
impl Drop for ScamperHostSvcb {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_svcb_free(self.inner) }; }
}
unsafe impl Send for ScamperHostSvcb {}

/// A DNS resource record.
pub struct ScamperHostRr { inner: *mut ScamperHostRrT }

impl ScamperHostRr {
    pub(crate) unsafe fn from_ptr(ptr: *mut ScamperHostRrT) -> Option<Self> {
        if ptr.is_null() { return None; }
        let ptr = unsafe { scamper_host::scamper_host_rr_use(ptr) };
        Some(ScamperHostRr { inner: ptr })
    }

    pub fn rr_class(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_class_get(self.inner) } }
    pub fn rr_type(&self) -> u16 { unsafe { scamper_host::scamper_host_rr_type_get(self.inner) } }
    pub fn name(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_rr_name_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }
    pub fn ttl(&self) -> u32 { unsafe { scamper_host::scamper_host_rr_ttl_get(self.inner) } }

    pub fn addr(&self) -> Option<ScamperAddr> {
        let ptr = unsafe { scamper_host::scamper_host_rr_addr_get(self.inner) };
        unsafe { ScamperAddr::from_ptr(ptr) }
    }

    pub fn str_val(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_rr_str_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }

    pub fn soa(&self) -> Option<ScamperHostSoa> {
        let ptr = unsafe { scamper_host::scamper_host_rr_soa_get(self.inner) };
        if ptr.is_null() { None } else {
            let ptr = unsafe { scamper_host::scamper_host_rr_soa_use(ptr) };
            Some(ScamperHostSoa { inner: ptr })
        }
    }

    pub fn mx(&self) -> Option<ScamperHostMx> {
        let ptr = unsafe { scamper_host::scamper_host_rr_mx_get(self.inner) };
        if ptr.is_null() { None } else {
            let ptr = unsafe { scamper_host::scamper_host_rr_mx_use(ptr) };
            Some(ScamperHostMx { inner: ptr })
        }
    }

    pub fn txt(&self) -> Option<ScamperHostTxt> {
        let ptr = unsafe { scamper_host::scamper_host_rr_txt_get(self.inner) };
        if ptr.is_null() { None } else {
            let ptr = unsafe { scamper_host::scamper_host_rr_txt_use(ptr) };
            Some(ScamperHostTxt { inner: ptr })
        }
    }

    pub fn opt(&self) -> Option<ScamperHostOpt> {
        let ptr = unsafe { scamper_host::scamper_host_rr_opt_get(self.inner) };
        if ptr.is_null() { None } else {
            let ptr = unsafe { scamper_host::scamper_host_rr_opt_use(ptr) };
            Some(ScamperHostOpt { inner: ptr })
        }
    }

    pub fn svcb(&self) -> Option<ScamperHostSvcb> {
        let ptr = unsafe { scamper_host::scamper_host_rr_svcb_get(self.inner) };
        if ptr.is_null() { None } else {
            let ptr = unsafe { scamper_host::scamper_host_rr_svcb_use(ptr) };
            Some(ScamperHostSvcb { inner: ptr })
        }
    }
}

impl Drop for ScamperHostRr {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_rr_free(self.inner) }; }
}
unsafe impl Send for ScamperHostRr {}
unsafe impl Sync for ScamperHostRr {}

/// A DNS query/response pair.
pub struct ScamperHostQuery { inner: *mut ScamperHostQueryT }

impl ScamperHostQuery {
    pub(crate) unsafe fn from_ptr(ptr: *mut ScamperHostQueryT) -> Option<Self> {
        if ptr.is_null() { return None; }
        let ptr = unsafe { scamper_host::scamper_host_query_use(ptr) };
        Some(ScamperHostQuery { inner: ptr })
    }

    pub fn tx(&self) -> Option<SystemTime> {
        timeval_to_systemtime(unsafe { scamper_host::scamper_host_query_tx_get(self.inner) })
    }
    pub fn rx(&self) -> Option<SystemTime> {
        timeval_to_systemtime(unsafe { scamper_host::scamper_host_query_rx_get(self.inner) })
    }
    pub fn rcode(&self) -> u8 { unsafe { scamper_host::scamper_host_query_rcode_get(self.inner) } }
    pub fn flags(&self) -> u8 { unsafe { scamper_host::scamper_host_query_flags_get(self.inner) } }
    pub fn id(&self) -> u16 { unsafe { scamper_host::scamper_host_query_id_get(self.inner) } }
    pub fn ancount(&self) -> u16 { unsafe { scamper_host::scamper_host_query_ancount_get(self.inner) } }
    pub fn nscount(&self) -> u16 { unsafe { scamper_host::scamper_host_query_nscount_get(self.inner) } }
    pub fn arcount(&self) -> u16 { unsafe { scamper_host::scamper_host_query_arcount_get(self.inner) } }

    pub fn an(&self, i: u16) -> Option<ScamperHostRr> {
        let ptr = unsafe { scamper_host::scamper_host_query_an_get(self.inner, i) };
        unsafe { ScamperHostRr::from_ptr(ptr) }
    }
    pub fn ns(&self, i: u16) -> Option<ScamperHostRr> {
        let ptr = unsafe { scamper_host::scamper_host_query_ns_get(self.inner, i) };
        unsafe { ScamperHostRr::from_ptr(ptr) }
    }
    pub fn ar(&self, i: u16) -> Option<ScamperHostRr> {
        let ptr = unsafe { scamper_host::scamper_host_query_ar_get(self.inner, i) };
        unsafe { ScamperHostRr::from_ptr(ptr) }
    }
    pub fn ar_opt(&self) -> Option<ScamperHostRr> {
        let ptr = unsafe { scamper_host::scamper_host_query_ar_opt_get(self.inner) };
        unsafe { ScamperHostRr::from_ptr(ptr) }
    }
}

impl Drop for ScamperHostQuery {
    fn drop(&mut self) { unsafe { scamper_host::scamper_host_query_free(self.inner) }; }
}
unsafe impl Send for ScamperHostQuery {}
unsafe impl Sync for ScamperHostQuery {}

/// A scamper DNS measurement result.
pub struct ScamperHost {
    pub(crate) inner: *mut ScamperHostT,
}

impl ScamperHost {
    pub(crate) unsafe fn from_ptr(ptr: *mut ScamperHostT) -> Option<Self> {
        if ptr.is_null() { return None; }
        Some(ScamperHost { inner: ptr })
    }

    pub fn src(&self) -> Option<ScamperAddr> {
        let ptr = unsafe { scamper_host::scamper_host_src_get(self.inner) };
        unsafe { ScamperAddr::from_ptr(ptr) }
    }

    pub fn dst(&self) -> Option<ScamperAddr> {
        let ptr = unsafe { scamper_host::scamper_host_dst_get(self.inner) };
        unsafe { ScamperAddr::from_ptr(ptr) }
    }

    pub fn userid(&self) -> u32 {
        unsafe { scamper_host::scamper_host_userid_get(self.inner) }
    }

    pub fn start(&self) -> Option<SystemTime> {
        timeval_to_systemtime(unsafe { scamper_host::scamper_host_start_get(self.inner) })
    }

    pub fn qname(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_qname_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }

    pub fn qtype(&self) -> u16 { unsafe { scamper_host::scamper_host_qtype_get(self.inner) } }
    pub fn qclass(&self) -> u16 { unsafe { scamper_host::scamper_host_qclass_get(self.inner) } }
    pub fn qcount(&self) -> u8 { unsafe { scamper_host::scamper_host_qcount_get(self.inner) } }
    pub fn flags(&self) -> u16 { unsafe { scamper_host::scamper_host_flags_get(self.inner) } }
    pub fn retries(&self) -> u8 { unsafe { scamper_host::scamper_host_retries_get(self.inner) } }

    pub fn stop(&self) -> HostStopReason {
        HostStopReason::from(unsafe { scamper_host::scamper_host_stop_get(self.inner) })
    }

    pub fn ecs(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_ecs_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }

    pub fn errmsg(&self) -> Option<&str> {
        let p = unsafe { scamper_host::scamper_host_errmsg_get(self.inner) };
        if p.is_null() { None } else { Some(unsafe { CStr::from_ptr(p) }.to_str().unwrap_or("")) }
    }

    pub fn query(&self, i: u8) -> Option<ScamperHostQuery> {
        let ptr = unsafe { scamper_host::scamper_host_query_get(self.inner, i) };
        unsafe { ScamperHostQuery::from_ptr(ptr) }
    }

    pub fn list(&self) -> Option<ScamperList> {
        let ptr = unsafe { scamper_host::scamper_host_list_get(self.inner) };
        unsafe { ScamperList::from_ptr(ptr) }
    }

    pub fn cycle(&self) -> Option<ScamperCycle> {
        let ptr = unsafe { scamper_host::scamper_host_cycle_get(self.inner) };
        unsafe { ScamperCycle::from_ptr(ptr) }
    }

    /// Convert to JSON string.
    pub fn to_json(&self) -> Option<String> {
        let mut len = 0usize;
        let ptr = unsafe { scamper_host::scamper_host_tojson(self.inner, &mut len) };
        if ptr.is_null() { return None; }
        let s = unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned();
        unsafe { libc::free(ptr as *mut libc::c_void) };
        Some(s)
    }
}

impl Drop for ScamperHost {
    fn drop(&mut self) {
        unsafe { scamper_host::scamper_host_free(self.inner) };
    }
}

unsafe impl Send for ScamperHost {}
unsafe impl Sync for ScamperHost {}