rscamper 0.2.2

Rust interface to scamper network measurement tool
Documentation
// rscamper - Rust wrapper for scamper vantage points
//
// 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, ScamperVpT};

/// A scamper vantage point.
pub struct ScamperVp {
    pub(crate) inner: *mut ScamperVpT,
}

impl ScamperVp {
    pub(crate) unsafe fn from_ptr(ptr: *mut ScamperVpT) -> Option<Self> {
        if ptr.is_null() { return None; }
        let ptr = unsafe { libscamperctrl::scamper_vp_use(ptr) };
        Some(ScamperVp { inner: ptr })
    }

    /// The full name of this VP.
    pub fn name(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_name_get(self.inner) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned())
        }
    }

    /// The short name of this VP, or the full name if no short name is set.
    pub fn shortname(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_shortname_get(self.inner) };
        if !ptr.is_null() {
            return Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned());
        }
        self.name()
    }

    /// The public IPv4 address of this VP.
    pub fn ipv4(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_ipv4_get(self.inner) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned())
        }
    }

    /// The IPv4 ASN of this VP. Returns an integer if parseable, otherwise
    /// the raw string.
    pub fn asn4(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_asn4_get(self.inner) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned())
        }
    }

    /// The ISO 3166 country code (upper-cased).
    pub fn cc(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_cc_get(self.inner) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().to_uppercase())
        }
    }

    /// The ISO 3166 state/region code (upper-cased).
    pub fn st(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_st_get(self.inner) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().to_uppercase())
        }
    }

    /// The populated place name.
    pub fn place(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_place_get(self.inner) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned())
        }
    }

    /// The nearest airport IATA code (upper-cased).
    pub fn iata(&self) -> Option<String> {
        let ptr = unsafe { libscamperctrl::scamper_vp_iata_get(self.inner) };
        if ptr.is_null() { None } else {
            Some(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().to_uppercase())
        }
    }

    /// The latitude and longitude of the VP, if known.
    pub fn loc(&self) -> Option<(f64, f64)> {
        let ptr = unsafe { libscamperctrl::scamper_vp_latlong_get(self.inner) };
        if ptr.is_null() { return None; }
        let s = unsafe { CStr::from_ptr(ptr) }.to_str().ok()?;
        let mut parts = s.splitn(2, ',');
        let lat = parts.next()?.parse().ok()?;
        let lon = parts.next()?.parse().ok()?;
        Some((lat, lon))
    }

    /// The tags associated with this VP.
    pub fn tags(&self) -> Vec<String> {
        let count = unsafe { libscamperctrl::scamper_vp_tagc_get(self.inner) };
        let mut tags = Vec::with_capacity(count);
        for i in 0..count {
            let ptr = unsafe { libscamperctrl::scamper_vp_tag_get(self.inner, i) };
            if !ptr.is_null() {
                tags.push(unsafe { CStr::from_ptr(ptr) }.to_string_lossy().into_owned());
            }
        }
        tags
    }
}

impl Drop for ScamperVp {
    fn drop(&mut self) {
        unsafe { libscamperctrl::scamper_vp_free(self.inner) };
    }
}

impl Clone for ScamperVp {
    fn clone(&self) -> Self {
        let ptr = unsafe { libscamperctrl::scamper_vp_use(self.inner) };
        ScamperVp { inner: ptr }
    }
}

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

impl Eq for ScamperVp {}

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

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

unsafe impl Send for ScamperVp {}
unsafe impl Sync for ScamperVp {}