bgpq3 0.1.0

A wrapper around the bgpq3/bgpq4 binary
Documentation
//! [<img alt="github" src="https://img.shields.io/badge/github-rappet/rust__bgpq3-8da0cb?style=for-the-badge&labelColor=555555&logo=github" height="20">](https://github.com/rappet/rust_bgpq3)
//! [<img alt="crates.io" src="https://img.shields.io/crates/v/bgpq3.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/bgpq3)
//! [<img alt="docs.rs" src="https://img.shields.io/badge/docs.rs-bgpq3-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K" height="20">](https://docs.rs/bgpq3)
//!
//! This library provides a thing wrapper around the [`bgpq3`]/[`bgpq4`] binary.
//!
//! See [`Bgpq3::query_v4`] and [`Bgpq3::query_v6`].
//!
//! ## Example
//!
//! bgpq3 AS-Set
//!
//! ```
//! extern crate bgpq3;
//!
//! pub fn main() {
//!     let networks = bgpq3::Bgpq3::new().query_v6("AS-RAPPET").unwrap();
//!     println!("{:?}", networks);
//! }
//! ```
//!
//! bgpq3 ASN
//!
//! ```
//! extern crate bgpq3;
//!
//! pub fn main() {
//!     let networks = bgpq3::Bgpq3::new().query_v6(207968).unwrap();
//!     println!("{:?}", networks);
//! }
//! ```
//!
//! bgpq4
//!
//! ```ignore
//! extern crate bgpq3;
//! use bgpq3::{Bgpq3, Version};
//!
//! pub fn main() {
//!     let bgpq4 = Bgpq3::builder().version(Version::Bgpq4).build();
//!     let networks = bgpq4.query_v6("AS-RAPPET").unwrap();
//!     println!("{:?}", networks);
//! }
//! ```
//!
//! ## Feature flags
//!
//! - `tokio` for async process invocation.
//!
//! [`bgpq3`]: https://github.com/snar/bgpq3
//! [`bgpq4`]: https://github.com/bgp/bgpq4

extern crate serde;
extern crate serde_json;
extern crate thiserror;
extern crate ipnetwork;
#[cfg(feature = "tokio")]
extern crate tokio;

use thiserror::Error;
use ipnetwork::{Ipv4Network, Ipv6Network, IpNetworkError};
use serde::Deserialize;
use std::process::Command;
use std::fmt::{Display, Formatter};

/// A wrapper around the `bgpq3` or `bgpq4` binary.
///
/// The default wrapper can be crated with [`Bgpq3::new`].
/// A custom wrapper can be build using the [`Bgpq3Settings`] builder.
///
/// The binaries have to be located in the `$PATH` environment variable for the default configuration.
///
/// # Example
///
/// Default wrapper using `bgpq3`:
///
/// ```
/// use bgpq3::Bgpq3;
///
/// let bgpq3 = Bgpq3::new();
/// ```
///
/// Custom wrapper using the recommended bgpq4 fork:
///
/// ```
/// use bgpq3::{Bgpq3, Version};
///
/// let bgpq4 = Bgpq3::builder().version(Version::Bgpq4).build();
/// ```
#[derive(Default, Clone)]
pub struct Bgpq3 {
    settings: Bgpq3Settings,
}

impl Bgpq3 {
    /// Creates a new [`Bgpq3`] wrapper with the default settings.
    pub fn new() -> Bgpq3 {
        Default::default()
    }

    /// Creates a new wrapper with bgpq3 as a backend.
    pub fn bgpq3() -> Bgpq3 {
        Bgpq3::builder().version(Version::Bgpq3).build()
    }

    /// Creates a new wrapper with bgpq4 as a backend.
    pub fn bgpq4() -> Bgpq3 {
        Bgpq3::builder().version(Version::Bgpq4).build()
    }

    /// Creates a new [`Bgpq3`] builder [`Bgpq3Settings`].
    pub fn builder() -> Bgpq3Settings {
        Bgpq3Settings::new()
    }

    /// Creates a new [`Bgpq3`] from a builder..
    pub fn with_settings(settings: &Bgpq3Settings) -> Bgpq3 {
        Bgpq3 {
            settings: settings.clone(),
        }
    }

    /// Queries a list of IPv4 networks.
    pub fn query_v4(&self, query: impl Into<Bgpq3Query>) -> Bgpq3Result<Vec<Ipv4Network>> {
        let output = query.into()
            .build_command(&self.settings, false)
            .output()?;
        if !output.status.success() {
            return Err(Bgpq3Error::StatusNotSuccess);
        }
        let deserialized: Bgpq3Output = serde_json::from_slice(output.stdout.as_slice())?;
        deserialized.prefixes_v4()
    }

    /// Queries a list of IPv6 networks.
    pub fn query_v6(&self, query: impl Into<Bgpq3Query>) -> Bgpq3Result<Vec<Ipv6Network>> {
        let output = query.into()
            .build_command(&self.settings, true)
            .output()?;
        if !output.status.success() {
            return Err(Bgpq3Error::StatusNotSuccess);
        }
        let deserialized: Bgpq3Output = serde_json::from_slice(output.stdout.as_slice())?;
        deserialized.prefixes_v6()
    }

    /// Queries a list of IPv4 networks using tokio.
    #[cfg(feature = "tokio")]
    pub async fn tokio_query_v4(&self, query: impl Into<Bgpq3Query>) -> Bgpq3Result<Vec<Ipv4Network>> {
        let output = tokio::process::Command::from(
            query.into()
                .build_command(&self.settings, false)
        )
            .output().await?;
        if !output.status.success() {
            return Err(Bgpq3Error::StatusNotSuccess);
        }
        let deserialized: Bgpq3Output = serde_json::from_slice(output.stdout.as_slice())?;
        deserialized.prefixes_v4()
    }

    /// Queries a list of IPv6 networks using tokio.
    #[cfg(feature = "tokio")]
    pub async fn tokio_query_v6(&self, query: impl Into<Bgpq3Query>) -> Bgpq3Result<Vec<Ipv6Network>> {
        let output = tokio::process::Command::from(
            query.into()
                .build_command(&self.settings, true)
        )
            .output().await?;
        if !output.status.success() {
            return Err(Bgpq3Error::StatusNotSuccess);
        }
        let deserialized: Bgpq3Output = serde_json::from_slice(output.stdout.as_slice())?;
        deserialized.prefixes_v6()
    }
}

#[derive(Deserialize)]
struct Bgpq3Output {
    #[serde(rename = "NN")]
    nn: Vec<Bgpq3OutputInner>,
}

impl Bgpq3Output {
    fn prefixes_v4(self) -> Bgpq3Result<Vec<Ipv4Network>> {
        let prefixes: Result<Vec<Ipv4Network>, IpNetworkError> = self.nn.into_iter()
            .map(|inner| inner.prefix.parse())
            .collect();
        Ok(prefixes?)
    }

    fn prefixes_v6(self) -> Bgpq3Result<Vec<Ipv6Network>> {
        let prefixes: Result<Vec<Ipv6Network>, IpNetworkError> = self.nn.into_iter()
            .map(|inner| inner.prefix.parse())
            .collect();
        Ok(prefixes?)
    }
}

#[derive(Deserialize)]
struct Bgpq3OutputInner {
    prefix: String,
}

/// A query for bgpq3.
///
/// See [`Bgpq3::query_v4`] and [`Bgpq3::query_v6`] for usage examples.
///
/// # Example
///
/// AS-SET
///
/// ```
/// use bgpq3::Bgpq3Query;
/// let query: Bgpq3Query = "AS-RAPPET".into();
/// ```
///
/// ASN
///
/// ```
/// use bgpq3::Bgpq3Query;
/// let query: Bgpq3Query = 207968.into();
/// ```
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Bgpq3Query {
   pub(crate) query_string: String,
}

impl Bgpq3Query {
    /// Build a query for an AS-SET
    ///
    /// # Examples
    ///
    /// ```
    /// use bgpq3::Bgpq3Query;
    /// let query = Bgpq3Query::as_set("AS-RAPPET");
    /// assert_eq!(query.to_string(), "AS-RAPPET");
    ///
    /// let query_from: Bgpq3Query = "AS-RAPPET".into();
    /// assert_eq!(query, query_from);
    /// ```
    pub fn as_set(as_set: &str) -> Bgpq3Query {
        Bgpq3Query {
            query_string: as_set.to_string(),
        }
    }

    /// Build a query for an ASN
    ///
    /// # Examples
    /// ```
    /// use bgpq3::Bgpq3Query;
    /// let query = Bgpq3Query::asn(207968);
    /// assert_eq!(query.to_string(), "AS207968");
    ///
    /// let query_from: Bgpq3Query = 207968.into();
    /// assert_eq!(query, query_from);
    /// ```
    pub fn asn(asn: u32) -> Bgpq3Query {
        Bgpq3Query {
            query_string: format!("AS{}", asn.to_string()),
        }
    }

    fn build_command(&self, settings: &Bgpq3Settings, ipv6: bool) -> Command {
        let mut command = Command::new(settings.version.bin_name());
        // JSON
        command.arg("-j");
        if ipv6 {
            command.arg("-6");
        }
        command.arg(&self.query_string);
        command
    }
}

impl Display for Bgpq3Query {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.query_string)
    }
}

impl From<&str> for Bgpq3Query {
    fn from(as_set: &str) -> Self {
        Bgpq3Query::as_set(as_set)
    }
}

impl From<u32> for Bgpq3Query {
    fn from(asn: u32) -> Self {
        Bgpq3Query::asn(asn)
    }
}

/// Settings to create a new [`Bgpq3`] wrapper.
///
/// [`Bgpq3`]: crate::Bgpq3
#[derive(Default, Debug, Clone)]
pub struct Bgpq3Settings {
    version: Version,
}

impl Bgpq3Settings {
    pub fn new() -> Bgpq3Settings {
        Default::default()
    }

    pub fn version(mut self, version: Version) -> Bgpq3Settings {
        self.version = version;
        self
    }

    pub fn build(self) -> Bgpq3 {
        Bgpq3::with_settings(&self)
    }
}

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Bgpq3Error {
    #[error("io: {0}")]
    Io(#[from] std::io::Error),
    #[error("bgpq3/bgpq4 did not run successfully")]
    StatusNotSuccess,
    #[error("failed parsing bgpq3/bgpq4 output json: {0}")]
    Json(#[from] serde_json::Error),
    #[error("failed parsing ip-network: {0}")]
    IpNetwork(#[from] IpNetworkError),
}

impl From<Bgpq3Error> for std::io::Error {
    fn from(err: Bgpq3Error) -> Self {
        use std::io::{Error, ErrorKind};

        match err {
            Bgpq3Error::Io(io) => io,
            Bgpq3Error::StatusNotSuccess => Error::new(ErrorKind::Other, err.to_string()),
            Bgpq3Error::Json(err) => err.into(),
            Bgpq3Error::IpNetwork(_) => Error::new(ErrorKind::InvalidData, err.to_string()),
        }
    }
}

pub type Bgpq3Result<T> = Result<T, Bgpq3Error>;

/// Selects, if the bgpq3 or bgpq4 binary is used.
///
/// [`bgpq3`] is used as default.
/// [`bgpq4`] should be preferred.
///
/// [`bgpq3`]: https://github.com/snar/bgpq3
/// [`bgpq4`]: https://github.com/bgp/bgpq4
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Version {
    Bgpq3,
    Bgpq4,
}

impl Version {
    pub fn bin_name(self) -> &'static str {
        match self {
            Version::Bgpq3 => "bgpq3",
            Version::Bgpq4 => "bgpq4"
        }
    }
}

impl Default for Version {
    fn default() -> Self {
        Version::Bgpq3
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}