titanite 0.3.0

Client/Server Library for Gemini protocol with Titan support
Documentation
pub struct Meta {
    pub size: usize,
    pub url: Url,
    pub mime: Option<String>,
    pub token: Option<String>,
    pub options: Option<IndexMap<String, String>>,
}

impl Meta {
    pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
        use crate::tool::Header;
        use regex::Regex;
        let header = from_utf8(buffer.header_bytes()?)?;
        Ok(Self {
            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 = IndexMap::new();
                        for option in v.as_str().split("&") {
                            let kv: Vec<&str> = option.split('=').collect();
                            if kv.len() == 2 {
                                if let Some(v) =
                                    options.insert(kv[0].to_string(), kv[1].to_string())
                                {
                                    bail!("Option key duplicate with value: {v}")
                                }
                            } else {
                                bail!("Invalid options format")
                            }
                        }
                        Some(options)
                    }
                    None => None,
                },
                None => None,
            },
            // * Titan URL is not compatible with [STD66](https://datatracker.ietf.org/doc/html/rfc3986)
            //   parse partially; see protocol specification for details
            url: match Regex::new(r"^([^;\?]+)")?.captures(header) {
                Some(c) => match c.get(1) {
                    Some(v) => Url::parse(v.as_str())?,
                    None => bail!("URL required"),
                },
                None => bail!("URL required"),
            },
        })
    }
    pub fn into_bytes(self) -> Vec<u8> {
        self.to_bytes()
    }
    pub fn to_bytes(&self) -> Vec<u8> {
        let mut meta = format!("{};size={}", self.url, self.size);
        if let Some(ref mime) = self.mime {
            meta.push_str(&format!(";mime={mime}"));
        }
        if let Some(ref token) = self.token {
            meta.push_str(&format!(";token={token}"));
        }
        if let Some(ref options) = self.options {
            meta.push('?');
            let mut items = Vec::new();
            for (k, v) in options {
                items.push(format!("{k}={v}"));
            }
            meta.push_str(&items.join("&"));
        }
        meta.push('\r');
        meta.push('\n');
        meta.into_bytes()
    }
}

#[test]
fn test() {
    const BYTES: &[u8] =
        "titan://geminiprotocol.net/raw/path;size=4;mime=text/plain;token=token?key1=value1&key2=value2\r\nDATA"
            .as_bytes();

    let meta = Meta::from_bytes(BYTES).unwrap().into_bytes();

    // println!("{:?}", from_utf8(&bytes));
    // println!("{:?}", from_utf8(&BYTES));

    assert_eq!(meta, BYTES[..BYTES.len() - 4]); // skip DATA
}

use anyhow::{bail, Result};
use indexmap::IndexMap;
use std::str::from_utf8;
use url::Url;