ic-bn-lib 0.2.3

Internet Computer Boundary Nodes shared modules
Documentation
// Needed for certain macros
#![recursion_limit = "256"]
#![warn(clippy::nursery)]
#![warn(tail_expr_drop_order)]
#![allow(clippy::cognitive_complexity)]
#![allow(clippy::field_reassign_with_default)]
#![allow(clippy::collapsible_if)]

#[cfg(feature = "custom-domains")]
pub mod custom_domains;
pub mod http;
pub mod network;
pub mod pubsub;
#[cfg(feature = "smtp")]
pub mod smtp;
pub mod tasks;
pub mod tests;
pub mod tls;
pub mod utils;
#[cfg(feature = "vector")]
pub mod vector;

use std::{fs::File, net::IpAddr, path::Path};

use anyhow::{Context, anyhow};
use bytes::Bytes;
use futures::StreamExt;
use ic_bn_lib_common::Error;
use serde::Serialize;
use tokio::io::AsyncWriteExt;

pub use hickory_proto;
pub use hickory_resolver;
pub use hyper;
pub use hyper_util;
pub use ic_agent;
pub use ic_bn_lib_common;
#[cfg(feature = "smtp")]
pub use mail_auth;
pub use prometheus;
#[cfg(feature = "acme")]
pub use rcgen;
pub use reqwest;
pub use rustls;
#[cfg(feature = "acme-alpn")]
pub use rustls_acme;
pub use uuid;

/// Converts a string representation to an `EmailAddress`. Panics when an error occurs.
#[macro_export]
macro_rules! email {
    ($email:expr) => {{ $crate::smtp::address::EmailAddress::from_text($email).unwrap() }};
}

/// Error to be used with `retry_async` macro
/// which indicates whether it should be retried or not.
#[derive(thiserror::Error, Debug)]
pub enum RetryError {
    #[error("Permanent error: {0:?}")]
    Permanent(anyhow::Error),
    #[error("Transient error: {0:?}")]
    Transient(anyhow::Error),
}

/// Downloads the given url to given path.
/// Destination folder must exist.
pub fn download_url_to(url: &str, path: &Path) -> Result<u64, Error> {
    let mut r = reqwest::blocking::get(url).context("unable to perform HTTP request")?;
    if !r.status().is_success() {
        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
    }

    let mut file = File::create(path).context("could not create file")?;
    Ok(r.copy_to(&mut file)
        .context("unable to write body to file")?)
}

/// Downloads the given url and returns it as Bytes
pub fn download_url(url: &str) -> Result<Bytes, Error> {
    let r = reqwest::blocking::get(url).context("unable to perform HTTP request")?;
    if !r.status().is_success() {
        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
    }

    Ok(r.bytes().context("unable to fetch file")?)
}

/// Downloads the given url to given path.
/// Destination folder must exist.
pub async fn download_url_to_async(url: &str, path: &Path) -> Result<(), Error> {
    let r = reqwest::get(url)
        .await
        .context("unable to perform HTTP request")?;
    if !r.status().is_success() {
        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
    }

    let mut file = tokio::fs::File::create(path)
        .await
        .context("could not create file")?;

    let mut stream = r.bytes_stream();
    while let Some(v) = stream.next().await {
        file.write(&v.context("unable to read chunk")?)
            .await
            .context("unable to write chunk")?;
    }

    Ok(())
}

/// Downloads the given url and returns it as Bytes
pub async fn download_url_async(url: &str) -> Result<Bytes, Error> {
    let r = reqwest::get(url)
        .await
        .context("unable to perform HTTP request")?;

    if !r.status().is_success() {
        return Err(anyhow!("incorrect HTTP code: {}", r.status()).into());
    }

    Ok(r.bytes().await.context("unable to fetch file")?)
}

/// Retrying async closures/functions holding mutable references is a pain in Rust.
/// So, for now, we'll have to use a macro to work that around.
#[macro_export]
macro_rules! retry_async {
    ($f:expr, $timeout:expr, $delay:expr) => {{
        use rand::{Rng, SeedableRng};
        // SmallRng is Send which we require
        let mut rng = rand::rngs::SmallRng::from_entropy();

        let start = std::time::Instant::now();
        let mut delay = $delay;

        let result = loop {
            // Run the function wrapping it into Tokio timeout future so
            // its execution time doesn't exceed our configured limit
            let Ok(res) = tokio::time::timeout($timeout, $f).await else {
                break Err(anyhow::anyhow!("Timed out"));
            };

            let err = match res {
                Ok(v) => break Ok(v),
                Err($crate::RetryError::Permanent(e)) => break Err(e),
                Err($crate::RetryError::Transient(e)) => e,
            };

            let left = $timeout.saturating_sub(start.elapsed());
            if left == std::time::Duration::ZERO {
                break Err(err);
            }

            delay = left.min(delay * 2);
            // Generate a random jitter in 0.0..0.1 range
            let jitter: f64 = (rng.r#gen::<f64>() / 10.0);
            let d64 = delay.as_secs_f64();
            delay = Duration::from_secs_f64(d64.mul_add(0.95, d64 * jitter));
            tokio::time::sleep(delay).await;
        };

        result
    }};

    ($f:expr, $timeout:expr) => {
        retry_async!($f, $timeout, Duration::from_millis(500))
    };

    ($f:expr) => {
        retry_async!($f, Duration::from_secs(60), Duration::from_millis(500))
    };
}

/// Returns family of an IP address
pub trait IpFamily {
    fn family(&self) -> &'static str;
}

impl IpFamily for IpAddr {
    fn family(&self) -> &'static str {
        if self.is_ipv4() { "v4" } else { "v6" }
    }
}

/// Converts bool to yes/no static str
pub trait BoolYesNo {
    fn yesno(&self) -> &'static str;
}

impl BoolYesNo for bool {
    fn yesno(&self) -> &'static str {
        if *self { "yes" } else { "no" }
    }
}

pub trait SerializeOption<T> {
    /// Serializes `Option<T>` as either inner value or provided default
    fn serialize_or<'t, O>(&'t self, otherwise: O) -> SerializeOr<'t, T, O>;
}

pub struct SerializeOr<'t, T, O> {
    option: &'t Option<T>,
    otherwise: O,
}

impl<'t, T: Serialize, O: Serialize> Serialize for SerializeOr<'t, T, O> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self.option {
            Some(v) => v.serialize(serializer),
            None => self.otherwise.serialize(serializer),
        }
    }
}

impl<T> SerializeOption<T> for Option<T> {
    fn serialize_or<'t, O>(&'t self, otherwise: O) -> SerializeOr<'t, T, O> {
        SerializeOr {
            option: self,
            otherwise,
        }
    }
}

#[macro_export]
macro_rules! dyn_event {
    ($lvl:ident, $($arg:tt)+) => {
        match $lvl {
            ::tracing::Level::TRACE => ::tracing::trace!($($arg)+),
            ::tracing::Level::DEBUG => ::tracing::debug!($($arg)+),
            ::tracing::Level::INFO => ::tracing::info!($($arg)+),
            ::tracing::Level::WARN => ::tracing::warn!($($arg)+),
            ::tracing::Level::ERROR => ::tracing::error!($($arg)+),
        }
    };
}

/// Truncates the given string to around n *bytes*,
/// on the closest UTF-8 code point boundary.
pub fn truncate(s: &str, n: usize) -> &str {
    let n = s.len().min(n);
    let m = (0..=n)
        .rfind(|m| s.is_char_boundary(*m))
        .unwrap_or_default();
    &s[..m]
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_truncate() {
        assert_eq!(truncate("foobarbaz", 4), "foob");
        assert_eq!(truncate("tättähäärä härkä", 12), "tättähää");
        assert_eq!(truncate("", 99), "");
        assert_eq!(truncate("🏁", 2), "");
        assert_eq!(truncate("foobarbaz", 99), "foobarbaz");
    }
}