titanite 0.2.0

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

pub const CODE: &[u8] = b"20";

/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success)
pub struct Default<'a> {
    pub mime: String,
    pub data: &'a [u8],
}

impl<'a> Default<'a> {
    /// Build `Self` from UTF-8 header bytes
    /// * expected buffer includes leading status code, message, CRLF
    pub fn from_bytes(buffer: &'a [u8]) -> Result<Self> {
        use crate::Header;
        use regex::Regex;
        use std::str::from_utf8;
        let h = buffer.header_bytes()?;
        if h.get(..2)
            .is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
        {
            bail!("Invalid status code")
        }
        Ok(Self {
            mime: match Regex::new(r"^([^\/]+\/[^\s;]+)")?.captures(from_utf8(&h[3..])?) {
                Some(c) => match c.get(1) {
                    Some(m) => m.as_str().to_string(),
                    None => bail!("Content type required"),
                },
                None => bail!("Could not parse content type"),
            },
            data: &buffer[h.len() + 2..],
        })
    }

    /// Convert `Self` into UTF-8 bytes presentation
    pub fn into_bytes(self) -> Vec<u8> {
        let mut bytes = Vec::with_capacity(3 + self.mime.len() + 2 + self.data.len());
        bytes.extend(CODE);
        bytes.push(b' ');
        bytes.extend(self.mime.into_bytes());
        bytes.extend([b'\r', b'\n']);
        bytes.extend(self.data);
        bytes
    }
}

#[test]
fn test() {
    let source = format!("20 text/gemini\r\ndata");
    let target = Default::from_bytes(source.as_bytes()).unwrap();

    assert_eq!(target.mime, "text/gemini".to_string());
    assert_eq!(target.data, "data".as_bytes());
    assert_eq!(target.into_bytes(), source.as_bytes());
}