n5i 0.12.0-dev.1

Common components for n5i
Documentation
// SPDX-FileCopyrightText: 2024-2026 The n5i Project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

mod cmd;
pub use cmd::split_with_quotes;

#[cfg(feature = "graphql")]
use async_graphql::{InputValueError, InputValueResult, Name, ScalarType, Value};
use hmac_sha256::HMAC;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fmt::{Debug, Display, Formatter};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
pub struct MultiLanguageItem(pub BTreeMap<String, String>);

impl Display for MultiLanguageItem {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let english_name = self.0.get("en").unwrap_or_else(|| {
            self.0
                .iter()
                .find(|(key, _)| key.starts_with("en"))
                .map_or_else(
                    || self.0.iter().next().map(|(_, value)| value).unwrap(),
                    |(_, value)| value,
                )
        });
        f.write_str(english_name)
    }
}

impl Debug for MultiLanguageItem {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

impl IntoIterator for MultiLanguageItem {
    type Item = (String, String);
    type IntoIter = std::collections::btree_map::IntoIter<String, String>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl MultiLanguageItem {
    #[must_use]
    pub fn from_string(default: String) -> Self {
        Self(BTreeMap::from([("en".to_string(), default)]))
    }
    #[must_use]
    pub fn from_hashmap(map: HashMap<String, String>) -> Self {
        Self(map.into_iter().collect())
    }
}

#[cfg(feature = "graphql")]
#[async_graphql::Scalar]
impl ScalarType for MultiLanguageItem {
    fn parse(value: Value) -> InputValueResult<Self> {
        if let Value::String(value) = value {
            // Parse the integer value
            Ok(Self(BTreeMap::from([("en".to_string(), value)])))
        } else if let Value::Object(value) = value {
            let mut map = BTreeMap::new();
            for (k, v) in value {
                if let Value::String(v) = v {
                    map.insert(k.as_str().to_string(), v);
                } else {
                    return Err(InputValueError::expected_type(v.clone()));
                }
            }
            Ok(Self(map))
        } else {
            Err(InputValueError::expected_type(value))
        }
    }

    fn to_value(&self) -> Value {
        Value::Object(
            self.0
                .clone()
                .into_iter()
                .map(|(k, v)| (Name::new(k), Value::String(v)))
                .collect(),
        )
    }
}
#[must_use]
pub fn derive_entropy(identifier: &str, n5i_seed: &str) -> String {
    let mut hasher = HMAC::new(n5i_seed);
    hasher.update(identifier.as_bytes());
    let result = hasher.finalize();
    hex::encode(result)
}

// Takes a list of types, and a function to call on each type, as well as arguments to pass to the function
#[macro_export]
macro_rules! for_each_type {
    ($func:ident, $($type:ty),*) => {
        $(
            $func::<$type>();
        )*
    };
    ($func:ident, $($type:ty),*; $call:tt) => {
        $(
            $func::<$type>$call;
        )*
    };
}

// Works like for_each_type, but assumes the function is async and runs the calls in parallel
#[macro_export]
macro_rules! for_each_type_parallel {
    ($func:ident, $($type:ty),*) => {
        tokio::join!(
            $(
                $func::<$type>()
            ),*
        ).await;
    };
    ($func:ident, $($type:ty),*; $call:tt) => {
        tokio::join!(
            $(
                $func::<$type>$call
            ),*
        ).await;
    };
}

// Works like for_each_type_parallel, but assumes the function returns a result, and if any of the results are an error, returns the first error
#[macro_export]
macro_rules! for_each_type_parallel_result {
    ($func:ident, $($type:ty),*) => {
        tokio::try_join!(
            $(
                $func::<$type>()
            ),*
        )
    };
    ($func:ident, $($type:ty),*; $call:tt) => {
        tokio::try_join!(
            $(
                async { $func::<$type>$call.await }
            ),*
        )
    };
}

#[must_use]
pub const fn is_ip_private(ip: IpAddr) -> bool {
    match ip {
        IpAddr::V4(ip) => {
            ip.octets()[0] == 0 // "This network"
                || ip.is_private()
                || is_shared_4(ip)
                || ip.is_loopback()
                || ip.is_link_local()
                // addresses reserved for future protocols (`192.0.0.0/24`)
                // .9 and .10 are documented as globally reachable so they're excluded
                || (
                ip.octets()[0] == 192 && ip.octets()[1] == 0 && ip.octets()[2] == 0
                    && ip.octets()[3] != 9 && ip.octets()[3] != 10
            )
                || ip.is_documentation()
                || is_benchmarking_4(ip)
                || is_reserved_4(ip)
                || ip.is_broadcast()
        }
        IpAddr::V6(ip) => {
            ip.is_unspecified()
                || ip.is_loopback()
                // IPv4-mapped Address (`::ffff:0:0/96`)
                || matches!(ip.segments(), [0, 0, 0, 0, 0, 0xffff, _, _])
                // IPv4-IPv6 Translat. (`64:ff9b:1::/48`)
                || matches!(ip.segments(), [0x64, 0xff9b, 1, _, _, _, _, _])
                // Discard-Only Address Block (`100::/64`)
                || matches!(ip.segments(), [0x100, 0, 0, 0, _, _, _, _])
                // IETF Protocol Assignments (`2001::/23`)
                || (matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b < 0x200)
                && !(
                // Port Control Protocol Anycast (`2001:1::1`)
                u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0001
                    // Traversal Using Relays around NAT Anycast (`2001:1::2`)
                    || u128::from_be_bytes(ip.octets()) == 0x2001_0001_0000_0000_0000_0000_0000_0002
                    // AMT (`2001:3::/32`)
                    || matches!(ip.segments(), [0x2001, 3, _, _, _, _, _, _])
                    // AS112-v6 (`2001:4:112::/48`)
                    || matches!(ip.segments(), [0x2001, 4, 0x112, _, _, _, _, _])
                    // ORCHIDv2 (`2001:20::/28`)
                    // Drone Remote ID Protocol Entity Tags (DETs) Prefix (`2001:30::/28`)`
                    || matches!(ip.segments(), [0x2001, b, _, _, _, _, _, _] if b >= 0x20 && b <= 0x3F)
            ))
                // 6to4 (`2002::/16`) – it's not explicitly documented as globally reachable,
                // IANA says N/A.
                || matches!(ip.segments(), [0x2002, _, _, _, _, _, _, _])
                || is_documentation_6(&ip)
                || is_unique_local_6(&ip)
                || is_unicast_link_local_6(&ip)
        }
    }
}

const fn is_reserved_4(ip: Ipv4Addr) -> bool {
    ip.octets()[0] & 240 == 240 && !ip.is_broadcast()
}

const fn is_benchmarking_4(ip: Ipv4Addr) -> bool {
    ip.octets()[0] == 198 && (ip.octets()[1] & 0xfe) == 18
}

const fn is_shared_4(ip: Ipv4Addr) -> bool {
    ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000 == 0b0100_0000)
}

const fn is_documentation_6(ip: &Ipv6Addr) -> bool {
    ip.segments()[0] == 0x2001 && ip.segments()[1] == 0xdb8
}

const fn is_unique_local_6(ip: &Ipv6Addr) -> bool {
    (ip.segments()[0] & 0xfe00) == 0xfc00
}

const fn is_unicast_link_local_6(ip: &Ipv6Addr) -> bool {
    (ip.segments()[0] & 0xffc0) == 0xfe80
}

// A helper for skipping deserialization of values that default to false
#[inline]
#[must_use]
pub const fn is_false(v: &bool) -> bool {
    !*v
}

// A helper for serde to make a bool field that defaults to true
#[inline]
#[must_use]
pub const fn true_default() -> bool {
    true
}