ice 0.4.0

A simple CTF tool store
Documentation
use anyhow::{Context, Result};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use crate::utils::utils;

fn is_b64_char(c: char) -> bool {
    matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' | '=' | ' ')
}

fn is_b32_char(c: char) -> bool {
    matches!(c, 'A'..='Z' | 'a'..='z' | '2'..='7' | '=' | ' ')
}

fn decode_or_encode<D>(s: &str, decode: D, encode: fn(&str) -> String) -> Result<String>
where
    D: FnOnce(&str) -> Option<Vec<u8>>,
{
    match decode(s.trim()) {
        Some(n) => {
            let decoded = String::from_utf8_lossy(&n);
            let result = decoded.into_owned();
            if !result.contains('\u{FFFD}') {
                return Ok(result);
            }
            Ok(encode(s))
        }
        None => Ok(encode(s)),
    }
}

pub fn b64(s: &str) -> Result<String> {
    let looks_like_b64 = !s.trim().is_empty() && s.trim().chars().all(is_b64_char);
    if looks_like_b64 {
        decode_or_encode(s, |s| STANDARD.decode(s).ok(), |s| STANDARD.encode(s))
    } else {
        Ok(STANDARD.encode(s))
    }
}

pub fn b32(s: &str) -> Result<String> {
    let looks_like_b32 = !s.trim().is_empty() && s.trim().chars().all(is_b32_char);
    if looks_like_b32 {
        decode_or_encode(s, |s| base32::decode(base32::Alphabet::RFC4648 { padding: true }, s), |s| {
            base32::encode(base32::Alphabet::RFC4648 { padding: true }, s.as_bytes())
        })
    } else {
        Ok(base32::encode(
            base32::Alphabet::RFC4648 { padding: true },
            s.as_bytes(),
        ))
    }
}

pub fn b45(s: &str) -> Result<String> {
    let s = s.trim();
    let looks_like = !s.is_empty() && s.chars().all(is_b45_char);
    if looks_like {
        decode_or_encode(s, |s| base45::decode(s).ok(), |s| base45::encode(s.as_bytes()))
    } else {
        Ok(base45::encode(s.as_bytes()))
    }
}

pub fn b58(s: &str) -> Result<String> {
    let s = s.trim();
    let looks_like = !s.is_empty() && s.chars().all(is_b58_char);
    if looks_like {
        decode_or_encode(s, |s| bs58::decode(s).into_vec().ok(), |s| bs58::encode(s).into_string())
    } else {
        Ok(bs58::encode(s).into_string())
    }
}

fn is_b45_char(c: char) -> bool {
    matches!(c, '0'..='9' | 'A'..='Z' | ' ' | '$' | '%' | '*' | '+' | '-' | '.' | '/' | ':')
}

fn is_b58_char(c: char) -> bool {
    matches!(c, '1'..='9' | 'A'..='Z' | 'a'..='z')
}

fn is_b62_char(c: char) -> bool {
    matches!(c, '0'..='9' | 'A'..='Z' | 'a'..='z')
}

fn b62_decode_str(s: &str) -> Option<Vec<u8>> {
    let s = s.trim();
    if s.is_empty() {
        return Some(Vec::new());
    }
    let mut out = Vec::with_capacity(s.len() * 7 / 10 + 1);
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        let group_len = (bytes.len() - i).min(10);
        let byte_len = match group_len {
            2 => 1,
            3 => 2,
            5 => 3,
            6 => 4,
            7 => 5,
            9 => 6,
            10 => 7,
            _ => return None,
        };
        let mut val: u64 = 0;
        for &b in &bytes[i..i + group_len] {
            val = match b {
                b'0'..=b'9' => val * 62 + (b - b'0') as u64,
                b'A'..=b'Z' => val * 62 + (b - b'A' + 10) as u64,
                b'a'..=b'z' => val * 62 + (b - b'a' + 36) as u64,
                _ => return None,
            };
        }
        if byte_len < 8 && val >= (1u64 << (byte_len * 8)) {
            return None;
        }
        let be = val.to_be_bytes();
        out.extend_from_slice(&be[8 - byte_len..]);
        i += group_len;
    }
    Some(out)
}

const B62_CHARSET: &[u8] =
    b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

fn b62_width(bytes: usize) -> usize {
    match bytes {
        1 => 2,
        2 => 3,
        3 => 5,
        4 => 6,
        5 => 7,
        6 => 9,
        7 => 10,
        _ => 0,
    }
}

fn b62_encode_bytes(bytes: &[u8]) -> String {
    if bytes.is_empty() {
        return String::new();
    }
    let mut out = String::with_capacity(bytes.len() + bytes.len() / 2);
    for chunk in bytes.chunks(7) {
        let mut val: u64 = 0;
        for &b in chunk {
            val = val * 256 + b as u64;
        }
        let width = b62_width(chunk.len());
        let mut digits = vec![b'0'; width];
        for i in (0..width).rev() {
            digits[i] = B62_CHARSET[(val % 62) as usize];
            val /= 62;
        }
        out.push_str(std::str::from_utf8(&digits).unwrap());
    }
    out
}

fn b62_encode_str(s: &str) -> String {
    b62_encode_bytes(s.as_bytes())
}

pub fn b62(s: &str) -> Result<String> {
    let s = s.trim();
    let looks_like_b62 = !s.is_empty() && s.chars().all(is_b62_char);
    if looks_like_b62 {
        decode_or_encode(s, b62_decode_str, b62_encode_str)
    } else {
        Ok(b62_encode_str(s))
    }
}

pub fn b85(s: &str) -> Result<String> {
    let is_b85 = s.starts_with("<~") && s.ends_with("~>");

    if is_b85 {
        let inner = &s[2..s.len() - 2];
        let inner = inner.replace(" ", "").replace("\n", "").replace("\r", "");
        let decoded = ascii85_decode(&inner)?;
        Ok(String::from_utf8_lossy(&decoded).into_owned())
    } else {
        let encoded = ascii85_encode(s.as_bytes());
        Ok(format!("<~{}~>", encoded))
    }
}

fn ascii85_encode(data: &[u8]) -> String {
    let mut result = String::new();
    for chunk in data.chunks(4) {
        let mut buf = [0u8; 4];
        for (i, &b) in chunk.iter().enumerate() {
            buf[i] = b;
        }
        let val = u32::from_be_bytes(buf);
        if val == 0 && chunk.len() == 4 {
            result.push('z');
        } else {
            let mut encoded = ['!'; 5];
            let n = chunk.len() + 1;
            let mut v = val;
            for i in (0..5).rev() {
                encoded[i] = char::from_u32((v % 85) + 33).unwrap();
                v /= 85;
            }
            for &c in &encoded[..n] {
                result.push(c);
            }
        }
    }
    result
}

fn ascii85_decode(data: &str) -> Result<Vec<u8>> {
    let mut result = Vec::new();
    let mut buf = Vec::new();

    for c in data.chars() {
        if c == 'z' {
            if !buf.is_empty() {
                return Err(anyhow::anyhow!("Invalid 'z' in Ascii85 data"));
            }
            result.extend_from_slice(&[0, 0, 0, 0]);
        } else if c == '~' {
            break;
        } else if c.is_ascii_whitespace() {
            continue;
        } else {
            let val = (c as u8).wrapping_sub(33);
            if val > 84 {
                return Err(anyhow::anyhow!("Invalid Ascii85 character: {}", c));
            }
            buf.push(val);
            if buf.len() == 5 {
                let num = buf.iter().fold(0u32, |acc, &v| acc * 85 + v as u32);
                result.extend_from_slice(&num.to_be_bytes());
                buf.clear();
            }
        }
    }

    if !buf.is_empty() {
        while buf.len() < 5 {
            buf.push(84);
        }
        let num = buf.iter().fold(0u32, |acc, &v| acc * 85 + v as u32);
        let bytes = num.to_be_bytes();
        let n = buf.len() - 1;
        result.extend_from_slice(&bytes[..n]);
    }

    Ok(result)
}

pub fn hexadecimal(s: &str) -> Result<String> {
    if utils::is_hex_repr(s) {
        let trimmed = s.trim();
        let bytes = hex::decode(trimmed).context("Invalid hex string")?;
        Ok(String::from_utf8_lossy(&bytes).into_owned())
    } else {
        let mut out = String::with_capacity(s.len() * 3);
        for c in s.chars() {
            out.push_str(&format!("{:x} ", u32::from(c)));
        }
        Ok(out.trim_end().to_string())
    }
}

pub fn octal(s: &str) -> Result<String> {
    if s.chars().all(|x| "01234567 ".contains(x)) {
        let chars: String = s
            .split_whitespace()
            .map(|c| {
                u8::from_str_radix(c, 8)
                    .map(|v| v as char)
                    .unwrap_or('?')
            })
            .collect();
        Ok(chars)
    } else {
        let mut out = String::with_capacity(s.len() * 4);
        for c in s.chars() {
            out.push_str(&format!("{:03o} ", u32::from(c)));
        }
        Ok(out.trim_end().to_string())
    }
}

pub fn binary(s: &str) -> Result<String> {
    if s.chars().all(|x| ['0', '1', ' '].contains(&x)) {
        let chars: String = utils::chunkify(s)
            .chunks(8)
            .map(|c| {
                u8::from_str_radix(&String::from_iter(c), 2)
                    .map(|v| v as char)
                    .unwrap_or('?')
            })
            .collect();
        Ok(chars)
    } else {
        let mut out = String::with_capacity(s.len() * 9);
        for c in s.chars() {
            out.push_str(&format!("{:08b} ", u32::from(c)));
        }
        Ok(out.trim_end().to_string())
    }
}