separate header member implementation, improve parser

This commit is contained in:
yggverse 2025-02-23 09:01:20 +02:00
parent e7fa73bf84
commit 3cb2296784
2 changed files with 121 additions and 104 deletions

View file

@ -1,123 +1,40 @@
use anyhow::{bail, Result}; pub mod header;
use std::{collections::HashMap, str::from_utf8}; pub use header::Header;
/// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) request /// [Titan](gemini://transjovian.org/titan/page/The%20Titan%20Specification) request
pub struct Titan<'a> { pub struct Titan<'a> {
pub data: &'a [u8], pub data: &'a [u8],
pub size: usize, pub header: Header,
pub url: String,
pub mime: Option<String>,
pub token: Option<String>,
pub options: Option<HashMap<String, String>>,
} }
impl<'a> Titan<'a> { impl<'a> Titan<'a> {
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; let data = buffer.get(header.to_bytes().len()..).unwrap_or(&[]);
let header = from_utf8(buffer.header_bytes()?)?; if header.size != data.len() {
Ok(Self { bail!("Data size mismatch ({}:{})", header.size, data.len())
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::<usize>() {
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")
} }
Ok(Self { data, header })
} }
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<u8> { pub fn into_bytes(self) -> Vec<u8> {
let mut header = format!("{};size={}", self.url, self.size); let mut bytes = self.header.into_bytes();
bytes.extend(self.data);
if let Some(mime) = self.mime { bytes
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] #[test]
fn test() { fn test() {
const DATA: &[u8] = &[1, 2, 3]; const BYTES: &[u8] =
const MIME: &str = "plain/text"; "titan://geminiprotocol.net/raw/path;size=4;mime=text/plain;token=token?key1=value1&key2=value2\r\nDATA"
const TOKEN: &str = "token"; .as_bytes();
let source = { let bytes = Titan::from_bytes(BYTES).unwrap().into_bytes();
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!( // println!("{:?}", std::str::from_utf8(&bytes));
"titan://geminiprotocol.net/raw/path;size={};mime={MIME};token={TOKEN}?key=value\r\n", // println!("{:?}", std::str::from_utf8(&BYTES));
DATA.len(),
)
.into_bytes();
target.extend_from_slice(DATA); assert_eq!(bytes, BYTES);
println!("{:?}", from_utf8(&source));
println!("{:?}", from_utf8(&target));
assert_eq!(source, target);
} }
use anyhow::{bail, Result};

100
src/request/titan/header.rs Normal file
View file

@ -0,0 +1,100 @@
pub struct Header {
pub size: usize,
pub url: String,
pub mime: Option<String>,
pub token: Option<String>,
pub options: Option<Vec<(String, String)>>,
}
impl Header {
pub fn from_bytes(buffer: &[u8]) -> Result<Self> {
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::<usize>() {
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<u8> {
self.to_bytes()
}
pub fn to_bytes(&self) -> Vec<u8> {
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;