i-dunno 0.6.0

RFC 8771 Internationalized Deliberately Unreadable Network Notation
Documentation
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::net::IpAddr;

use crate::bytesbits::BytesBits;
use crate::combinations::Combinations;
use crate::{confusion_level, ConfusionLevel};

/// Encode an IP address into a random, valid I-DUNNO representation at the
/// given confusion level.
///
/// Returns None if there are no possible encodings.
///
/// If there are many possible encodings, this function generates 10, and
/// chooses one of those at random.
///
/// The output of this function MAY be presented to humans, as recommended
/// by RFC8771.
///
/// # Examples
///
/// ```
/// use std::net::{Ipv4Addr, Ipv6Addr};
/// use i_dunno::{encode, ConfusionLevel};
/// let x = encode(
///     "0.0.1.1".parse().unwrap(),
///     ConfusionLevel::Delightful,
/// );
/// assert_eq!(x, None);
///
/// let a: String = encode(
///     "::16:164".parse().unwrap(),
///     ConfusionLevel::Minimum,
/// ).unwrap();
/// assert_eq!(
///     a,
///     "\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{2c02}d"
/// )
/// ```
pub fn encode(
    ip_addr: IpAddr,
    required_confusion_level: ConfusionLevel,
) -> Option<String> {
    encode_all(ip_addr, required_confusion_level)
        .take(10)
        .collect::<Vec<String>>()
        .choose(&mut thread_rng())
        .cloned()
}

/// Provide valid I-DUNNO encodings for the supplied IP address at the
/// given confusion level.
///
/// The output of this function should not be presented to humans, since
/// seeing multiple encodings may trigger an undesirable confusion reduction.
///
/// # Examples
///
/// ```
/// use i_dunno::{encode_all, ConfusionLevel};
/// let a: Vec<String> = encode_all(
///     "::16:164".parse().unwrap(),
///     ConfusionLevel::Minimum
/// ).collect();
/// assert_eq!(
///     a,
///     vec![
///         "\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{0}\u{2c02}d"
///     ]
/// );
///
/// let b = encode_all(
///     "198.51.100.164".parse().unwrap(),
///     ConfusionLevel::Minimum
/// ).next().unwrap();
/// assert_eq!(b, "c\u{c}l\u{04A4}");
/// ```
pub fn encode_all(
    ip_addr: IpAddr,
    required_confusion_level: ConfusionLevel,
) -> impl Iterator<Item = String> {
    let addr_octets = match ip_addr {
        IpAddr::V4(addr_v4) => addr_v4.octets().to_vec(),
        IpAddr::V6(addr_v6) => addr_v6.octets().to_vec(),
    };
    Combinations::new(BytesBits::new(addr_octets))
        .filter_map(string_from_u32s)
        .filter(move |candidate| {
            confusion_level(candidate)
                .map(|lev| lev >= required_confusion_level)
                .unwrap_or(false)
        })
}

fn string_from_u32s(u32s: Vec<u32>) -> Option<String> {
    let mut ret = String::with_capacity(u32s.len() * 4);
    for n in u32s {
        if let Some(ch) = std::char::from_u32(n) {
            ret.push(ch);
        } else {
            return None;
        }
    }
    Some(ret)
}

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

    #[test]
    fn encoding_of_something_with_no_valid_encodings() {
        assert_eq!(
            encode("0.0.0.1".parse().unwrap(), ConfusionLevel::Minimum),
            None
        );
        assert_eq!(enc("0.0.0.1", ConfusionLevel::Minimum), Vec::<&str>::new());
    }

    #[test]
    fn encoding_of_something_with_only_one_encoding() {
        // Since we have only 1 answer, we can test encode() as well as
        // encode_all().
        assert_eq!(
            to_hex_str(
                &encode("0.0.1.1".parse().unwrap(), ConfusionLevel::Minimum)
                    .unwrap()
            ),
            "000000c481"
        );

        assert_eq!(enc("0.0.1.1", ConfusionLevel::Minimum), vec!["000000c481"]);
        assert_eq!(
            enc("0.0.1.1", ConfusionLevel::Satisfactory),
            vec!["000000c481"]
        );
        assert_eq!(
            enc("0.0.1.1", ConfusionLevel::Delightful),
            Vec::<&str>::new()
        );
    }

    #[test]
    fn encode_localhost() {
        assert_eq!(
            enc("127.0.0.1", ConfusionLevel::Delightful),
            vec!["3fd0800001", "cfb8000001"]
        );
    }

    #[test]
    fn encoding_something_with_only_one_delightful_but_more_satisfactory() {
        assert_eq!(
            enc("124.32.3.10", ConfusionLevel::Minimum),
            vec!["3e0800cc8a", "cfa100060a"]
        );
        assert_eq!(
            enc("124.32.3.10", ConfusionLevel::Satisfactory),
            vec!["3e0800cc8a", "cfa100060a"]
        );
        assert_eq!(
            enc("124.32.3.10", ConfusionLevel::Delightful),
            vec!["3e0800cc8a"]
        );
    }

    #[test]
    fn all_encodings_of_198_51_100_164() {
        assert_eq!(
            enc("198.51.100.164", ConfusionLevel::Minimum),
            vec!["630c6cd2a4", "630cdb8924", "d8b14d4924"]
        );
        assert_eq!(
            enc("198.51.100.164", ConfusionLevel::Satisfactory),
            vec!["630c6cd2a4", "630cdb8924", "d8b14d4924"]
        );
        assert_eq!(
            enc("198.51.100.164", ConfusionLevel::Delightful),
            vec!["630c6cd2a4", "630cdb8924", "d8b14d4924"]
        );
    }

    #[test]
    fn encoding_an_ipv6_address() {
        assert_eq!(
            enc("::16:164", ConfusionLevel::Minimum),
            vec!["000000000000000000000000000000e2b08264"]
        );
        assert_eq!(
            enc("::16:164", ConfusionLevel::Satisfactory),
            vec!["000000000000000000000000000000e2b08264"]
        );
        assert_eq!(
            enc("::16:164", ConfusionLevel::Delightful),
            vec!["000000000000000000000000000000e2b08264"]
        );
    }

    #[test]
    fn encoding_an_ipv6_address_with_lots_of_options() {
        let encs = enc("2001:db8::8a2e:370:7334", ConfusionLevel::Delightful);
        assert!(encs
            .contains(&String::from("1000215b400000000000000851380670e78cb4")));
        assert!(encs.contains(&String::from(
            "c480041b38000000000000010ac5b00dd88eccb4"
        )));
    }

    fn enc(s: &str, confusion_level: ConfusionLevel) -> Vec<String> {
        to_hex(encode_all(s.parse().unwrap(), confusion_level).collect())
    }

    fn to_hex(enc: Vec<String>) -> Vec<String> {
        enc.iter().map(|s| to_hex_str(s)).collect::<Vec<String>>()
    }

    fn to_hex_str(s: &str) -> String {
        s.as_bytes().iter().map(|c| format!("{:02x}", c)).collect()
    }
}