mirror of
https://github.com/YGGverse/titanite.git
synced 2026-03-31 17:15:33 +00:00
separate header member implementation, improve parser
This commit is contained in:
parent
e7fa73bf84
commit
3cb2296784
2 changed files with 121 additions and 104 deletions
|
|
@ -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) {
|
Ok(Self { data, 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
100
src/request/titan/header.rs
Normal 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;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue