implement separated header mod for success default status

This commit is contained in:
yggverse 2025-02-23 10:10:19 +02:00
parent 3aaacdd656
commit 7c62a393ed
4 changed files with 75 additions and 37 deletions

View file

@ -84,7 +84,7 @@ fn test() {
match target { match target {
Response::Success(ref this) => match this { Response::Success(ref this) => match this {
Success::Default(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()); assert_eq!(this.data, "data".as_bytes());
} }
}, },
@ -99,7 +99,7 @@ fn test() {
match target { match target {
Response::Redirect(ref this) => match this { 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!(),
}, },
_ => panic!(), _ => panic!(),
@ -113,7 +113,7 @@ fn test() {
match target { match target {
Response::Redirect(ref this) => match this { 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!(),
}, },
_ => panic!(), _ => panic!(),

View file

@ -30,13 +30,13 @@ impl<'a> Success<'a> {
#[test] #[test]
fn 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(); let source = Success::from_bytes(request.as_bytes()).unwrap();
match source { match source {
Success::Default(ref this) => { Success::Default(ref this) => {
assert_eq!(this.mime, "text/gemini".to_string()); assert_eq!(this.header.mime, "text/gemini");
assert_eq!(this.data, "data".as_bytes()); assert_eq!(this.data, "DATA".as_bytes());
} }
} }
assert_eq!(source.into_bytes(), request.as_bytes()); assert_eq!(source.into_bytes(), request.as_bytes());

View file

@ -1,45 +1,27 @@
use anyhow::{bail, Result}; pub mod header;
pub use header::Header;
pub const CODE: &[u8] = b"20";
/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) /// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success)
pub struct Default<'a> { pub struct Default<'a> {
pub mime: String,
pub data: &'a [u8], pub data: &'a [u8],
pub header: Header,
} }
impl<'a> Default<'a> { impl<'a> Default<'a> {
/// Build `Self` from UTF-8 header bytes /// Build `Self` from UTF-8 header bytes
/// * expected buffer includes leading status code, message, CRLF /// * expected buffer includes leading status code, message, CRLF
pub fn from_bytes(buffer: &'a [u8]) -> Result<Self> { pub fn from_bytes(buffer: &'a [u8]) -> Result<Self> {
use crate::Header; let header = Header::from_bytes(buffer)?;
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 { Ok(Self {
mime: match Regex::new(r"^([^\/]+\/[^\s;]+)")?.captures(from_utf8(&h[3..])?) { data: buffer.get(header.to_bytes().len()..).unwrap_or(&[]),
Some(c) => match c.get(1) { header,
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 /// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> { pub fn into_bytes(self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(3 + self.mime.len() + 2 + self.data.len()); let mut bytes = Vec::new();
bytes.extend(CODE); bytes.extend(self.header.into_bytes());
bytes.push(b' ');
bytes.extend(self.mime.into_bytes());
bytes.extend([b'\r', b'\n']);
bytes.extend(self.data); bytes.extend(self.data);
bytes bytes
} }
@ -47,10 +29,11 @@ impl<'a> Default<'a> {
#[test] #[test]
fn test() { fn test() {
let source = format!("20 text/gemini\r\ndata"); const BYTES: &[u8] = "20 text/gemini\r\nDATA".as_bytes();
let target = Default::from_bytes(source.as_bytes()).unwrap(); let default = Default::from_bytes(BYTES).unwrap();
assert_eq!(target.mime, "text/gemini".to_string()); assert_eq!(default.header.mime, "text/gemini".to_string());
assert_eq!(target.data, "data".as_bytes()); assert_eq!(default.into_bytes(), BYTES);
assert_eq!(target.into_bytes(), source.as_bytes());
} }
use anyhow::Result;

View file

@ -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<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"),
},
})
}
/// Convert `Self` into UTF-8 bytes presentation
pub fn into_bytes(self) -> Vec<u8> {
self.to_bytes()
}
pub fn to_bytes(&self) -> Vec<u8> {
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};