gempress 0.1.1

An Express.js inspired server framework for the gemini protocol
Documentation
use crate::error::{GempressError, GempressResult};
use native_tls::TlsStream;
use std::{io::Write, net::TcpStream, str::FromStr};
use url::Url;

#[derive(Debug, PartialEq, Eq)]
pub struct Request {
    pub url: Url,
}

impl Request {
    pub fn parse(request: &str) -> GempressResult<Self> {
        Self::from_str(request)
    }

    /// Get file path
    pub fn file_path(&self) -> &str {
        self.url
            .path()
            .chars()
            .next()
            .map_or("", |c| &self.url.path()[c.len_utf8()..])
    }
}

impl FromStr for Request {
    type Err = GempressError;

    fn from_str(s: &str) -> GempressResult<Self> {
        let mut s = s.to_string();

        // Add gemini: scheme if not explicitly set
        if s.starts_with("//") {
            s = format!("gemini:{}", s);
        }

        // Check protocol
        if let Some(proto_end) = s.find("://") {
            // If set, check if it's allowed
            let protocol = &s[..proto_end];

            if protocol != "gemini" {
                // TODO: return 53 error instead of dropping
                return Err(GempressError::InvalidRequest("invalid protocol".into()));
            }
        } else {
            // If no protocol is found, gemini: is implied
            s = format!("gemini://{}", s);
        }

        // Extract and parse the url from the request.
        let raw = s
            .trim_end_matches(0x0 as char)
            .strip_suffix("\r\n")
            .ok_or_else(|| GempressError::InvalidRequest("malformed request".into()))?;
        let url = Url::parse(&raw)
            .map_err(|e| GempressError::InvalidRequest(format!("invalid url: {}", e)))?;

        Ok(Self { url })
    }
}

pub struct Response {
    stream: TlsStream<TcpStream>,
    pub status: [u8; 2],
    pub meta: Vec<u8>,
    pub body: Vec<u8>,
}

impl Response {
    pub(crate) fn new(stream: TlsStream<TcpStream>) -> Self {
        Self {
            status: [b'2', b'0'],
            meta: "text/gemini".into(),
            body: Vec::new(),
            stream,
        }
    }

    pub fn send(&mut self, text: &[u8]) -> GempressResult<usize> {
        let mut buf: Vec<u8> = Vec::new();

        // <Status>
        buf.extend(&self.status);

        // <Space>
        buf.push(0x20);

        // <Meta>
        buf.extend(&self.meta);

        buf.extend(b"\r\n");

        buf.extend(self.body.clone());

        buf.extend(text);

        self.stream
            .write(&buf)
            .map_err(GempressError::StreamWriteFailed)
    }
}

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

    fn check_request(raw: &str, expected_url: &str) {
        let req = Request::parse(raw).unwrap();

        assert_eq!(
            req,
            Request {
                url: Url::parse(expected_url).unwrap()
            }
        );
    }

    #[test]
    fn parse_request() {
        check_request("gemini://example.space\r\n", "gemini://example.space");
    }

    #[test]
    fn parse_without_scheme() {
        check_request("example.space\r\n", "gemini://example.space");
    }

    #[test]
    fn parse_without_scheme_double_slash() {
        check_request("//example.space\r\n", "gemini://example.space");
    }

    #[test]
    fn parse_malformed_request() {
        let raw = "gemini://example.space";

        match Request::parse(raw) {
            Err(GempressError::InvalidRequest(_)) => {}
            x => panic!("expected GempressError::InvalidRequest, got: {:?}", x),
        }
    }
}