From 91077d3420e5f2c071704aa26a350ccc69339152 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 22 Feb 2025 20:25:01 +0200 Subject: [PATCH] add implement `data` member, use global `Header` trait --- Cargo.toml | 2 +- src/response.rs | 15 +++++---- src/response/success.rs | 13 ++++---- src/response/success/default.rs | 55 ++++++++++++++------------------- 4 files changed, 40 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c6cd49e..b2e3a5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "titanite" -version = "0.1.2" +version = "0.2.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/response.rs b/src/response.rs index 843eccd..883abe6 100644 --- a/src/response.rs +++ b/src/response.rs @@ -13,16 +13,16 @@ pub use success::Success; use anyhow::{bail, Result}; /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) source -pub enum Response { +pub enum Response<'a> { Certificate(Certificate), Failure(Failure), Input(Input), Redirect(Redirect), - Success(Success), + Success(Success<'a>), } -impl Response { - pub fn from_bytes(buffer: &[u8]) -> Result { +impl<'a> Response<'a> { + pub fn from_bytes(buffer: &'a [u8]) -> Result { match buffer.first() { Some(byte) => Ok(match byte { b'1' => Self::Input(Input::from_bytes(buffer)?), @@ -78,12 +78,15 @@ fn test() { } // 20 { - let source = format!("20 text/gemini\r\n"); + let source = format!("20 text/gemini\r\ndata"); let target = Response::from_bytes(source.as_bytes()).unwrap(); match target { Response::Success(ref this) => match this { - Success::Default(this) => assert_eq!(this.mime, "text/gemini".to_string()), + Success::Default(this) => { + assert_eq!(this.mime, "text/gemini".to_string()); + assert_eq!(this.data, "data".as_bytes()); + } }, _ => panic!(), } diff --git a/src/response/success.rs b/src/response/success.rs index 3f3e44a..4bfd3bd 100644 --- a/src/response/success.rs +++ b/src/response/success.rs @@ -4,12 +4,12 @@ pub use default::Default; use anyhow::{bail, Result}; /// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) -pub enum Success { - Default(Default), +pub enum Success<'a> { + Default(Default<'a>), } -impl Success { - pub fn from_bytes(buffer: &[u8]) -> Result { +impl<'a> Success<'a> { + pub fn from_bytes(buffer: &'a [u8]) -> Result { if buffer.first().is_none_or(|b| *b != b'2') { bail!("Unexpected first byte") } @@ -30,12 +30,13 @@ impl Success { #[test] fn test() { - let request = format!("20 text/gemini\r\n"); + let request = format!("20 text/gemini\r\ndata"); let source = Success::from_bytes(request.as_bytes()).unwrap(); match source { Success::Default(ref this) => { - assert_eq!(this.mime, "text/gemini".to_string()) + assert_eq!(this.mime, "text/gemini".to_string()); + assert_eq!(this.data, "data".as_bytes()); } } assert_eq!(source.into_bytes(), request.as_bytes()); diff --git a/src/response/success/default.rs b/src/response/success/default.rs index 7d443cb..ffef86e 100644 --- a/src/response/success/default.rs +++ b/src/response/success/default.rs @@ -3,63 +3,54 @@ use anyhow::{bail, Result}; pub const CODE: &[u8] = b"20"; /// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) -pub struct Default { +pub struct Default<'a> { pub mime: String, + pub data: &'a [u8], } -impl Default { +impl<'a> Default<'a> { /// Build `Self` from UTF-8 header bytes /// * expected buffer includes leading status code, message, CRLF - pub fn from_bytes(buffer: &[u8]) -> Result { - // calculate length once - let len = buffer.len(); - // validate headers for this response type - if !(3..=1024).contains(&len) { - bail!("Unexpected header length") - } - if buffer - .get(..2) + pub fn from_bytes(buffer: &'a [u8]) -> Result { + 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") } - // collect data bytes - let mut m = Vec::with_capacity(len); - for b in buffer[3..].iter() { - if *b == b'\r' { - continue; - } - if *b == b'\n' { - break; - } - m.push(*b) - } - Ok(Self { - mime: if m.is_empty() { - bail!("Content type required") - } else { - String::from_utf8(m)? + 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 { - let mut bytes = Vec::with_capacity(self.mime.len() + 5); + 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 request = format!("20 text/gemini\r\n"); - let source = Default::from_bytes(request.as_bytes()).unwrap(); + let source = format!("20 text/gemini\r\ndata"); + let target = Default::from_bytes(source.as_bytes()).unwrap(); - assert_eq!(source.mime, "text/gemini".to_string()); - assert_eq!(source.into_bytes(), request.as_bytes()); + assert_eq!(target.mime, "text/gemini".to_string()); + assert_eq!(target.data, "data".as_bytes()); + assert_eq!(target.into_bytes(), source.as_bytes()); }