rscamper 0.2.2

Rust interface to scamper network measurement tool
Documentation
// rscamper - Rust wrapper for scamper addresses
//
// 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::cmp::Ordering;
use std::ffi::{CStr, CString};
use std::fmt;
use std::hash::{Hash, Hasher};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

use crate::ffi::scamper_addr::{
    self, ScamperAddrT,
    SCAMPER_ADDR_TYPE_IPV4, SCAMPER_ADDR_TYPE_IPV6,
};

/// Represents a scamper network address (IPv4, IPv6, or Ethernet).
pub struct ScamperAddr {
    inner: *mut ScamperAddrT,
}

impl ScamperAddr {
    /// Wrap an existing raw pointer (takes ownership, increments refcount).
    pub(crate) unsafe fn from_ptr(ptr: *mut ScamperAddrT) -> Option<Self> {
        if ptr.is_null() {
            return None;
        }
        let ptr = unsafe { scamper_addr::scamper_addr_use(ptr) };
        Some(ScamperAddr { inner: ptr })
    }

    /// Parse an address from a string.
    ///
    /// The `kind` parameter is one of `SCAMPER_ADDR_TYPE_*`.
    pub fn from_str_typed(kind: i32, addr: &str) -> Option<Self> {
        let s = CString::new(addr).ok()?;
        let ptr = unsafe { scamper_addr::scamper_addr_fromstr(kind, s.as_ptr()) };
        if ptr.is_null() {
            None
        } else {
            Some(ScamperAddr { inner: ptr })
        }
    }

    /// Parse an IPv4 or IPv6 address from a string.
    pub fn from_str(addr: &str) -> Option<Self> {
        // Try IPv4 first, then IPv6
        Self::from_str_typed(SCAMPER_ADDR_TYPE_IPV4, addr)
            .or_else(|| Self::from_str_typed(SCAMPER_ADDR_TYPE_IPV6, addr))
    }

    /// Return the address type.
    pub fn addr_type(&self) -> i32 {
        unsafe { scamper_addr::scamper_addr_type_get(self.inner) }
    }

    /// Return true if this is an IPv4 address.
    pub fn is_ipv4(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_isipv4(self.inner) != 0 }
    }

    /// Return true if this is an IPv6 address.
    pub fn is_ipv6(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_isipv6(self.inner) != 0 }
    }

    /// Return true if this is an Ethernet address.
    pub fn is_ethernet(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_isethernet(self.inner) != 0 }
    }

    /// Return true if this is a link-local address.
    pub fn is_linklocal(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_islinklocal(self.inner) != 0 }
    }

    /// Return true if this is an RFC1918 private address.
    pub fn is_rfc1918(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_isrfc1918(self.inner) != 0 }
    }

    /// Return true if this is a unicast address.
    pub fn is_unicast(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_isunicast(self.inner) != 0 }
    }

    /// Return true if this is a 6to4 address.
    pub fn is_6to4(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_is6to4(self.inner) != 0 }
    }

    /// Return true if this is a reserved address.
    pub fn is_reserved(&self) -> bool {
        unsafe { scamper_addr::scamper_addr_isreserved(self.inner) != 0 }
    }

    /// Return the raw bytes of the address.
    pub fn packed(&self) -> Vec<u8> {
        let len = unsafe { scamper_addr::scamper_addr_len_get(self.inner) };
        let ptr = unsafe { scamper_addr::scamper_addr_addr_get(self.inner) };
        if ptr.is_null() || len == 0 {
            return Vec::new();
        }
        unsafe { std::slice::from_raw_parts(ptr as *const u8, len).to_vec() }
    }

    /// Convert to a standard Rust IpAddr if possible.
    pub fn to_ipaddr(&self) -> Option<IpAddr> {
        if self.is_ipv4() {
            let bytes = self.packed();
            if bytes.len() == 4 {
                let arr: [u8; 4] = bytes.try_into().ok()?;
                return Some(IpAddr::V4(Ipv4Addr::from(arr)));
            }
        } else if self.is_ipv6() {
            let bytes = self.packed();
            if bytes.len() == 16 {
                let arr: [u8; 16] = bytes.try_into().ok()?;
                return Some(IpAddr::V6(Ipv6Addr::from(arr)));
            }
        }
        None
    }
}

impl Drop for ScamperAddr {
    fn drop(&mut self) {
        unsafe { scamper_addr::scamper_addr_free(self.inner) };
    }
}

impl Clone for ScamperAddr {
    fn clone(&self) -> Self {
        let ptr = unsafe { scamper_addr::scamper_addr_use(self.inner) };
        ScamperAddr { inner: ptr }
    }
}

impl fmt::Display for ScamperAddr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut buf = vec![0u8; 64];
        let ptr = unsafe {
            scamper_addr::scamper_addr_tostr(
                self.inner,
                buf.as_mut_ptr() as *mut libc::c_char,
                buf.len(),
            )
        };
        if ptr.is_null() {
            write!(f, "<invalid>")
        } else {
            let s = unsafe { CStr::from_ptr(ptr) };
            write!(f, "{}", s.to_string_lossy())
        }
    }
}

impl fmt::Debug for ScamperAddr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ScamperAddr({})", self)
    }
}

impl PartialEq for ScamperAddr {
    fn eq(&self, other: &Self) -> bool {
        unsafe { scamper_addr::scamper_addr_cmp(self.inner, other.inner) == 0 }
    }
}

impl Eq for ScamperAddr {}

impl PartialOrd for ScamperAddr {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for ScamperAddr {
    fn cmp(&self, other: &Self) -> Ordering {
        let r = unsafe { scamper_addr::scamper_addr_cmp(self.inner, other.inner) };
        r.cmp(&0)
    }
}

impl Hash for ScamperAddr {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.packed().hash(state);
        self.addr_type().hash(state);
    }
}

unsafe impl Send for ScamperAddr {}
unsafe impl Sync for ScamperAddr {}