From 3cb229678455660c5891136c43334fd591b86734 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 23 Feb 2025 09:01:20 +0200 Subject: [PATCH] separate header member implementation, improve parser --- src/request/titan.rs | 125 ++++++------------------------------ src/request/titan/header.rs | 100 +++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 104 deletions(-) create mode 100644 src/request/titan/header.rs diff --git a/src/request/titan.rs b/src/request/titan.rs index 591bb16..7c5daf5 100644 --- a/src/request/titan.rs +++ b/src/request/titan.rs @@ -1,123 +1,40 @@ -use anyhow::{bail, Result}; -use std::{collections::HashMap, str::from_utf8}; +pub mod header; +pub use header::Header; /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) request pub struct Titan<'a> { pub data: &'a [u8], - pub size: usize, - pub url: String, - pub mime: Option, - pub token: Option, - pub options: Option>, + pub header: Header, } impl<'a> Titan<'a> { pub fn from_bytes(buffer: &'a [u8]) -> Result { - use crate::Header; - use regex::Regex; - let header = from_utf8(buffer.header_bytes()?)?; - Ok(Self { - data: &buffer[header.len() + 2..], - size: match Regex::new(r"size=(\d+)")?.captures(header) { - Some(c) => match c.get(1) { - Some(v) => match v.as_str().parse::() { - Ok(s) => s, - Err(e) => bail!(e), - }, - None => bail!("Size required"), - }, - None => bail!("Size required"), - }, - mime: match Regex::new(r"mime=([^\/]+\/[^\s;]+)")?.captures(header) { - Some(c) => c.get(1).map(|v| v.as_str().to_string()), - None => None, - }, - token: match Regex::new(r"token=([^\s;]+)")?.captures(header) { - Some(c) => c.get(1).map(|v| v.as_str().to_string()), - None => None, - }, - options: match Regex::new(r"\?(.*)$")?.captures(header) { - Some(c) => match c.get(1) { - Some(v) => { - let mut options = HashMap::new(); - for option in v.as_str().split("&") { - let kv: Vec<&str> = option.split('=').collect(); - if kv.len() == 2 { - options.insert(kv[0].to_string(), kv[1].to_string()); - } else { - bail!("Invalid options format") - } - } - Some(options) - } - None => None, - }, - None => None, - }, - url: match Regex::new(r"^([^;]+)")?.captures(header) { - Some(c) => match c.get(1) { - Some(v) => v.as_str().to_string(), - None => bail!("URL required"), - }, - None => bail!("URL required"), - }, - }) + let header = Header::from_bytes(buffer)?; + let data = buffer.get(header.to_bytes().len()..).unwrap_or(&[]); + if header.size != data.len() { + bail!("Data size mismatch ({}:{})", header.size, data.len()) + } + Ok(Self { data, header }) } - pub fn into_bytes(self) -> Vec { - let mut header = format!("{};size={}", self.url, self.size); - - if let Some(mime) = self.mime { - header.push_str(&format!(";mime={mime}")); - } - if let Some(token) = self.token { - header.push_str(&format!(";token={token}")); - } - if let Some(options) = self.options { - header.push('?'); - for (k, v) in options { - header.push_str(&format!("{k}={v}")); - } - } - header.push('\r'); - header.push('\n'); - - let mut request = header.into_bytes(); - request.extend(self.data); - request + let mut bytes = self.header.into_bytes(); + bytes.extend(self.data); + bytes } } #[test] fn test() { - const DATA: &[u8] = &[1, 2, 3]; - const MIME: &str = "plain/text"; - const TOKEN: &str = "token"; + const BYTES: &[u8] = + "titan://geminiprotocol.net/raw/path;size=4;mime=text/plain;token=token?key1=value1&key2=value2\r\nDATA" + .as_bytes(); - let source = { - let mut options = HashMap::new(); - options.insert("key".to_string(), "value".to_string()); - Titan { - data: DATA, - size: DATA.len(), - mime: Some(MIME.to_string()), - token: Some(TOKEN.to_string()), - options: Some(options), - url: "titan://geminiprotocol.net/raw/path".to_string(), - } - } - .into_bytes(); + let bytes = Titan::from_bytes(BYTES).unwrap().into_bytes(); - let mut target = format!( - "titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n", - DATA.len(), - ) - .into_bytes(); + // println!("{:?}", std::str::from_utf8(&bytes)); + // println!("{:?}", std::str::from_utf8(&BYTES)); - target.extend_from_slice(DATA); - - println!("{:?}", from_utf8(&source)); - println!("{:?}", from_utf8(&target)); - - assert_eq!(source, target); + assert_eq!(bytes, BYTES); } + +use anyhow::{bail, Result}; diff --git a/src/request/titan/header.rs b/src/request/titan/header.rs new file mode 100644 index 0000000..09ab508 --- /dev/null +++ b/src/request/titan/header.rs @@ -0,0 +1,100 @@ +pub struct Header { + pub size: usize, + pub url: String, + pub mime: Option, + pub token: Option, + pub options: Option>, +} + +impl Header { + pub fn from_bytes(buffer: &[u8]) -> Result { + use crate::Header; + use regex::Regex; + let header = from_utf8(buffer.header_bytes()?)?; + Ok(Self { + size: match Regex::new(r"size=(\d+)")?.captures(header) { + Some(c) => match c.get(1) { + Some(v) => match v.as_str().parse::() { + Ok(s) => s, + Err(e) => bail!(e), + }, + None => bail!("Size required"), + }, + None => bail!("Size required"), + }, + mime: match Regex::new(r"mime=([^\/]+\/[^\s;\?]+)")?.captures(header) { + Some(c) => c.get(1).map(|v| v.as_str().to_string()), + None => None, + }, + token: match Regex::new(r"token=([^\s;\?]+)")?.captures(header) { + Some(c) => c.get(1).map(|v| v.as_str().to_string()), + None => None, + }, + options: match Regex::new(r"\?(.*)$")?.captures(header) { + Some(c) => match c.get(1) { + Some(v) => { + let mut options = Vec::new(); + for option in v.as_str().split("&") { + let kv: Vec<&str> = option.split('=').collect(); + if kv.len() == 2 { + options.push((kv[0].to_string(), kv[1].to_string())) + } else { + bail!("Invalid options format") + } + } + Some(options) + } + None => None, + }, + None => None, + }, + url: match Regex::new(r"^([^;\?]+)")?.captures(header) { + Some(c) => match c.get(1) { + Some(v) => v.as_str().to_string(), + None => bail!("URL required"), + }, + None => bail!("URL required"), + }, + }) + } + pub fn into_bytes(self) -> Vec { + self.to_bytes() + } + pub fn to_bytes(&self) -> Vec { + let mut header = format!("{};size={}", self.url, self.size); + if let Some(ref mime) = self.mime { + header.push_str(&format!(";mime={mime}")); + } + if let Some(ref token) = self.token { + header.push_str(&format!(";token={token}")); + } + if let Some(ref options) = self.options { + header.push('?'); + let mut items = Vec::new(); + for (k, v) in options { + items.push(format!("{k}={v}")); + } + header.push_str(&items.join("&")); + } + header.push('\r'); + header.push('\n'); + header.into_bytes() + } +} + +#[test] +fn test() { + const BYTES: &[u8] = + "titan://geminiprotocol.net/raw/path;size=4;mime=text/plain;token=token?key1=value1&key2=value2\r\nDATA" + .as_bytes(); + + let bytes = Header::from_bytes(BYTES).unwrap().into_bytes(); + + // println!("{:?}", from_utf8(&bytes)); + // println!("{:?}", from_utf8(&BYTES)); + + assert_eq!(bytes, BYTES[..BYTES.len() - 4]); // skip DATA +} + +use anyhow::{bail, Result}; +use std::str::from_utf8;