add implement data member, use global Header trait

This commit is contained in:
yggverse 2025-02-22 20:25:01 +02:00
parent 385f9aefc4
commit 91077d3420
4 changed files with 40 additions and 45 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "titanite" name = "titanite"
version = "0.1.2" version = "0.2.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"

View file

@ -13,16 +13,16 @@ pub use success::Success;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
/// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) source /// [Gemini](https://geminiprotocol.net/docs/protocol-specification.gmi) source
pub enum Response { pub enum Response<'a> {
Certificate(Certificate), Certificate(Certificate),
Failure(Failure), Failure(Failure),
Input(Input), Input(Input),
Redirect(Redirect), Redirect(Redirect),
Success(Success), Success(Success<'a>),
} }
impl Response { impl<'a> Response<'a> {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> { pub fn from_bytes(buffer: &'a [u8]) -> Result<Self> {
match buffer.first() { match buffer.first() {
Some(byte) => Ok(match byte { Some(byte) => Ok(match byte {
b'1' => Self::Input(Input::from_bytes(buffer)?), b'1' => Self::Input(Input::from_bytes(buffer)?),
@ -78,12 +78,15 @@ fn test() {
} }
// 20 // 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(); let target = Response::from_bytes(source.as_bytes()).unwrap();
match target { match target {
Response::Success(ref this) => match this { 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!(), _ => panic!(),
} }

View file

@ -4,12 +4,12 @@ pub use default::Default;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) /// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success)
pub enum Success { pub enum Success<'a> {
Default(Default), Default(Default<'a>),
} }
impl Success { impl<'a> Success<'a> {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> { pub fn from_bytes(buffer: &'a [u8]) -> Result<Self> {
if buffer.first().is_none_or(|b| *b != b'2') { if buffer.first().is_none_or(|b| *b != b'2') {
bail!("Unexpected first byte") bail!("Unexpected first byte")
} }
@ -30,12 +30,13 @@ impl Success {
#[test] #[test]
fn 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(); 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.mime, "text/gemini".to_string());
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

@ -3,63 +3,54 @@ use anyhow::{bail, Result};
pub const CODE: &[u8] = b"20"; 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 { pub struct Default<'a> {
pub mime: String, pub mime: String,
pub data: &'a [u8],
} }
impl Default { 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: &[u8]) -> Result<Self> { pub fn from_bytes(buffer: &'a [u8]) -> Result<Self> {
// calculate length once use crate::Header;
let len = buffer.len(); use regex::Regex;
// validate headers for this response type use std::str::from_utf8;
if !(3..=1024).contains(&len) { let h = buffer.header_bytes()?;
bail!("Unexpected header length") if h.get(..2)
}
if buffer
.get(..2)
.is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1]) .is_none_or(|c| c[0] != CODE[0] || c[1] != CODE[1])
{ {
bail!("Invalid status code") 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 { Ok(Self {
mime: if m.is_empty() { mime: match Regex::new(r"^([^\/]+\/[^\s;]+)")?.captures(from_utf8(&h[3..])?) {
bail!("Content type required") Some(c) => match c.get(1) {
} else { Some(m) => m.as_str().to_string(),
String::from_utf8(m)? 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(self.mime.len() + 5); let mut bytes = Vec::with_capacity(3 + self.mime.len() + 2 + self.data.len());
bytes.extend(CODE); bytes.extend(CODE);
bytes.push(b' '); bytes.push(b' ');
bytes.extend(self.mime.into_bytes()); bytes.extend(self.mime.into_bytes());
bytes.extend([b'\r', b'\n']); bytes.extend([b'\r', b'\n']);
bytes.extend(self.data);
bytes bytes
} }
} }
#[test] #[test]
fn test() { fn test() {
let request = format!("20 text/gemini\r\n"); let source = format!("20 text/gemini\r\ndata");
let source = Default::from_bytes(request.as_bytes()).unwrap(); let target = Default::from_bytes(source.as_bytes()).unwrap();
assert_eq!(source.mime, "text/gemini".to_string()); assert_eq!(target.mime, "text/gemini".to_string());
assert_eq!(source.into_bytes(), request.as_bytes()); assert_eq!(target.data, "data".as_bytes());
assert_eq!(target.into_bytes(), source.as_bytes());
} }