diff --git a/Cargo.toml b/Cargo.toml index c9b11a6..7fb9990 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ categories = ["network-programming", "parser-implementations", "parsing"] repository = "https://github.com/YGGverse/titanite" [dependencies] +anyhow = "1.0.96" +regex = "1.11.1" diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..822decb --- /dev/null +++ b/src/request.rs @@ -0,0 +1,32 @@ +pub mod gemini; +pub mod titan; + +pub use gemini::Gemini; +pub use titan::Titan; + +use anyhow::{bail, Result}; + +pub enum Request<'a> { + Gemini(Gemini), + Titan(Titan<'a>), +} + +impl<'a> Request<'a> { + pub fn from_bytes(buffer: &'a [u8]) -> Result { + match buffer.first() { + Some(b) => match b { + b'g' => Ok(Self::Gemini(Gemini::from_bytes(buffer)?)), + b't' => Ok(Self::Titan(Titan::from_bytes(buffer)?)), + _ => bail!("Leading byte error"), + }, + None => bail!("Empty request"), + } + } + + pub fn into_bytes(self) -> Vec { + match self { + Self::Gemini(this) => this.into_bytes(), + Self::Titan(this) => this.into_bytes(), + } + } +} diff --git a/src/request/gemini.rs b/src/request/gemini.rs new file mode 100644 index 0000000..8186f82 --- /dev/null +++ b/src/request/gemini.rs @@ -0,0 +1,29 @@ +use anyhow::{bail, Result}; + +pub struct Gemini { + pub url: String, +} + +impl Gemini { + pub fn from_bytes(buffer: &[u8]) -> Result { + let mut l: usize = 0; + for b in buffer { + l += 1; + if l > 1024 { + bail!("Max header length reached!") + } + if *b == b'\r' { + continue; + } + if *b == b'\n' { + break; + } + } + Ok(Self { + url: String::from_utf8(buffer[..l].to_vec())?, + }) + } + pub fn into_bytes(self) -> Vec { + self.url.to_string().into_bytes() + } +} diff --git a/src/request/titan.rs b/src/request/titan.rs new file mode 100644 index 0000000..6f24da2 --- /dev/null +++ b/src/request/titan.rs @@ -0,0 +1,137 @@ +use anyhow::{bail, Result}; +use std::{collections::HashMap, str::from_utf8}; + +pub struct Titan<'a> { + pub data: &'a [u8], + pub size: usize, + pub url: String, + pub mime: Option, + pub token: Option, + pub options: Option>, +} + +impl<'a> Titan<'a> { + pub fn from_bytes(buffer: &'a [u8]) -> Result { + use regex::Regex; + + let mut l: usize = 0; + + for b in buffer { + l += 1; + if l > 1024 { + bail!("Max header length reached!") + } + if *b == b'\r' { + continue; + } + if *b == b'\n' { + break; + } + } + + let header = from_utf8(&buffer[..l])?; + Ok(Self { + data: &buffer[l..], + 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"), + }, + }) + } + + 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 + } +} + +#[test] +fn test_bytes() { + const DATA: &[u8] = &[1, 2, 3]; + const MIME: &str = "plain/text"; + const TOKEN: &str = "token"; + + 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 mut target = format!( + "titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n", + DATA.len(), + ) + .into_bytes(); + + target.extend_from_slice(DATA); + + println!("{:?}", from_utf8(&source)); + println!("{:?}", from_utf8(&target)); + + assert_eq!(source, target); +}