titanite 0.2.0

Client/Server Library for Gemini protocol with Titan support
Documentation
use anyhow::{bail, Result};
use std::{collections::HashMap, str::from_utf8};

/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) request
pub struct Titan<'a> {
    pub data: &'a [u8],
    pub size: usize,
    pub url: String,
    pub mime: Option<String>,
    pub token: Option<String>,
    pub options: Option<HashMap<String, String>>,
}

impl<'a> Titan<'a> {
    pub fn from_bytes(buffer: &'a [u8]) -> Result<Self> {
        use crate::Header;
        use regex::Regex;
        let header = from_utf8(buffer.header_bytes()?)?;
        Ok(Self {
            data: &buffer[header.len() + 2..],
            size: match Regex::new(r"size=(\d+)")?.captures(header) {
                Some(c) => match c.get(1) {
                    Some(v) => match v.as_str().parse::<usize>() {
                        Ok(s) => s,
                        Err(e) => bail!(e),
                    },
                    None => bail!("Size required"),
                },
                None => bail!("Size required"),
            },
            mime: match Regex::new(r"mime=([^\/]+\/[^\s;]+)")?.captures(header) {
                Some(c) => c.get(1).map(|v| v.as_str().to_string()),
                None => None,
            },
            token: match Regex::new(r"token=([^\s;]+)")?.captures(header) {
                Some(c) => c.get(1).map(|v| v.as_str().to_string()),
                None => None,
            },
            options: match Regex::new(r"\?(.*)$")?.captures(header) {
                Some(c) => match c.get(1) {
                    Some(v) => {
                        let mut options = HashMap::new();
                        for option in v.as_str().split("&") {
                            let kv: Vec<&str> = option.split('=').collect();
                            if kv.len() == 2 {
                                options.insert(kv[0].to_string(), kv[1].to_string());
                            } else {
                                bail!("Invalid options format")
                            }
                        }
                        Some(options)
                    }
                    None => None,
                },
                None => None,
            },
            url: match Regex::new(r"^([^;]+)")?.captures(header) {
                Some(c) => match c.get(1) {
                    Some(v) => v.as_str().to_string(),
                    None => bail!("URL required"),
                },
                None => bail!("URL required"),
            },
        })
    }

    pub fn into_bytes(self) -> Vec<u8> {
        let mut header = format!("{};size={}", self.url, self.size);

        if let Some(mime) = self.mime {
            header.push_str(&format!(";mime={mime}"));
        }
        if let Some(token) = self.token {
            header.push_str(&format!(";token={token}"));
        }
        if let Some(options) = self.options {
            header.push('?');
            for (k, v) in options {
                header.push_str(&format!("{k}={v}"));
            }
        }
        header.push('\r');
        header.push('\n');

        let mut request = header.into_bytes();
        request.extend(self.data);
        request
    }
}

#[test]
fn test() {
    const DATA: &[u8] = &[1, 2, 3];
    const MIME: &str = "plain/text";
    const TOKEN: &str = "token";

    let source = {
        let mut options = HashMap::new();
        options.insert("key".to_string(), "value".to_string());
        Titan {
            data: DATA,
            size: DATA.len(),
            mime: Some(MIME.to_string()),
            token: Some(TOKEN.to_string()),
            options: Some(options),
            url: "titan://geminiprotocol.net/raw/path".to_string(),
        }
    }
    .into_bytes();

    let mut target = format!(
        "titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n",
        DATA.len(),
    )
    .into_bytes();

    target.extend_from_slice(DATA);

    println!("{:?}", from_utf8(&source));
    println!("{:?}", from_utf8(&target));

    assert_eq!(source, target);
}