diff --git a/src/response.rs b/src/response.rs index 883abe6..8aa5284 100644 --- a/src/response.rs +++ b/src/response.rs @@ -84,7 +84,7 @@ fn test() { match target { Response::Success(ref this) => match this { Success::Default(this) => { - assert_eq!(this.mime, "text/gemini".to_string()); + assert_eq!(this.header.mime, "text/gemini"); assert_eq!(this.data, "data".as_bytes()); } }, @@ -99,7 +99,7 @@ fn test() { match target { Response::Redirect(ref this) => match this { - Redirect::Temporary(this) => assert_eq!(this.target, "target".to_string()), + Redirect::Temporary(this) => assert_eq!(this.target, "target"), _ => panic!(), }, _ => panic!(), @@ -113,7 +113,7 @@ fn test() { match target { Response::Redirect(ref this) => match this { - Redirect::Permanent(this) => assert_eq!(this.target, "target".to_string()), + Redirect::Permanent(this) => assert_eq!(this.target, "target"), _ => panic!(), }, _ => panic!(), diff --git a/src/response/success.rs b/src/response/success.rs index 4bfd3bd..2ef9605 100644 --- a/src/response/success.rs +++ b/src/response/success.rs @@ -30,13 +30,13 @@ impl<'a> Success<'a> { #[test] fn test() { - let request = format!("20 text/gemini\r\ndata"); + 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.data, "data".as_bytes()); + assert_eq!(this.header.mime, "text/gemini"); + 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 ffef86e..1a1fae7 100644 --- a/src/response/success/default.rs +++ b/src/response/success/default.rs @@ -1,45 +1,27 @@ -use anyhow::{bail, Result}; - -pub const CODE: &[u8] = b"20"; +pub mod header; +pub use header::Header; /// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) pub struct Default<'a> { - pub mime: String, pub data: &'a [u8], + pub header: Header, } 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 { - 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") - } + let header = Header::from_bytes(buffer)?; 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..], + data: buffer.get(header.to_bytes().len()..).unwrap_or(&[]), + header, }) } /// Convert `Self` into UTF-8 bytes presentation pub fn into_bytes(self) -> Vec { - 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']); + let mut bytes = Vec::new(); + bytes.extend(self.header.into_bytes()); bytes.extend(self.data); bytes } @@ -47,10 +29,11 @@ impl<'a> Default<'a> { #[test] fn test() { - let source = format!("20 text/gemini\r\ndata"); - let target = Default::from_bytes(source.as_bytes()).unwrap(); + const BYTES: &[u8] = "20 text/gemini\r\nDATA".as_bytes(); + let default = Default::from_bytes(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()); + assert_eq!(default.header.mime, "text/gemini".to_string()); + assert_eq!(default.into_bytes(), BYTES); } + +use anyhow::Result; diff --git a/src/response/success/default/header.rs b/src/response/success/default/header.rs new file mode 100644 index 0000000..4c1d4f7 --- /dev/null +++ b/src/response/success/default/header.rs @@ -0,0 +1,55 @@ +pub const CODE: &[u8] = b"20"; + +pub struct Header { + pub mime: String, +} + +impl Header { + /// Build `Self` from UTF-8 header bytes + /// * expected buffer includes leading status code, message, CRLF + pub fn from_bytes(buffer: &[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") + } + 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"), + }, + }) + } + + /// Convert `Self` into UTF-8 bytes presentation + pub fn into_bytes(self) -> Vec { + self.to_bytes() + } + + pub fn to_bytes(&self) -> Vec { + let mut bytes = Vec::with_capacity(3 + self.mime.len() + 2); + bytes.extend(CODE); + bytes.push(b' '); + bytes.extend(self.mime.as_bytes()); + bytes.extend([b'\r', b'\n']); + bytes + } +} + +#[test] +fn test() { + const BYTES: &[u8] = "20 text/gemini\r\nDATA".as_bytes(); + let header = Header::from_bytes(BYTES).unwrap(); + + assert_eq!(header.mime, "text/gemini".to_string()); + assert_eq!(header.into_bytes(), BYTES[..BYTES.len() - 4]); // skip DATA +} + +use anyhow::{bail, Result};