begin header holder implementation with lazy parser by getters, add request::Mode, add common header_bytes helper

This commit is contained in:
yggverse 2025-03-24 06:50:08 +02:00
parent a12a73d311
commit 7c518cecf6
13 changed files with 267 additions and 151 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ggemini" name = "ggemini"
version = "0.17.3" version = "0.18.0"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
readme = "README.md" readme = "README.md"

View file

@ -43,7 +43,7 @@ use gio::*;
use glib::*; use glib::*;
use ggemini::client::{ use ggemini::client::{
connection::{Request, Response}, connection::{request::{Mode, Request}, Response},
Client, Client,
}; };
@ -51,6 +51,7 @@ fn main() -> ExitCode {
Client::new().request_async( Client::new().request_async(
Request::Gemini { // or `Request::Titan` Request::Gemini { // or `Request::Titan`
uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(),
mode: Mode::Header // handle content separately (based on MIME)
}, },
Priority::DEFAULT, Priority::DEFAULT,
Cancellable::new(), Cancellable::new(),

View file

@ -3,7 +3,7 @@ pub mod request;
pub mod response; pub mod response;
pub use error::Error; pub use error::Error;
pub use request::Request; pub use request::{Mode, Request};
pub use response::Response; pub use response::Response;
// Local dependencies // Local dependencies
@ -74,36 +74,42 @@ impl Connection {
Some(&cancellable.clone()), Some(&cancellable.clone()),
move |result| match result { move |result| match result {
Ok(_) => match request { Ok(_) => match request {
Request::Gemini { .. } => Response::from_connection_async( Request::Gemini { mode, .. } => match mode {
self, Mode::All => todo!(),
priority, Mode::Header => Response::header_from_connection_async(
cancellable, self,
|result, connection| { priority,
callback(match result { cancellable,
Ok(response) => Ok((response, connection)), |result, connection| {
Err(e) => Err(Error::Response(e)), callback(match result {
}) Ok(response) => Ok((response, connection)),
}, Err(e) => Err(Error::Response(e)),
), })
},
),
},
// Make sure **all data bytes** sent to the destination // Make sure **all data bytes** sent to the destination
// > A partial write is performed with the size of a message block, which is 16kB // > A partial write is performed with the size of a message block, which is 16kB
// > https://docs.openssl.org/3.0/man3/SSL_write/#notes // > https://docs.openssl.org/3.0/man3/SSL_write/#notes
Request::Titan { data, .. } => output_stream.write_all_async( Request::Titan { data, mode, .. } => output_stream.write_all_async(
data, data,
priority, priority,
Some(&cancellable.clone()), Some(&cancellable.clone()),
move |result| match result { move |result| match result {
Ok(_) => Response::from_connection_async( Ok(_) => match mode {
self, Mode::All => todo!(),
priority, Mode::Header => Response::header_from_connection_async(
cancellable, self,
|result, connection| { priority,
callback(match result { cancellable,
Ok(response) => Ok((response, connection)), |result, connection| {
Err(e) => Err(Error::Response(e)), callback(match result {
}) Ok(response) => Ok((response, connection)),
}, Err(e) => Err(Error::Response(e)),
), })
},
),
},
Err((b, e)) => callback(Err(Error::Request(b, e))), Err((b, e)) => callback(Err(Error::Request(b, e))),
}, },
), ),
@ -124,12 +130,12 @@ impl Connection {
} }
} }
// Helpers // Tools
/// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html) /// Setup new [TlsClientConnection](https://docs.gtk.org/gio/iface.TlsClientConnection.html)
/// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html) /// wrapper for [SocketConnection](https://docs.gtk.org/gio/class.SocketConnection.html)
/// using `server_identity` as the [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication) /// using `server_identity` as the [SNI](https://geminiprotocol.net/docs/protocol-specification.gmi#server-name-indication)
pub fn new_tls_client_connection( fn new_tls_client_connection(
socket_connection: &SocketConnection, socket_connection: &SocketConnection,
server_identity: Option<&NetworkAddress>, server_identity: Option<&NetworkAddress>,
is_session_resumption: bool, is_session_resumption: bool,

View file

@ -1,5 +1,8 @@
pub mod error; pub mod error;
pub mod mode;
pub use error::Error; pub use error::Error;
pub use mode::Mode;
// Local dependencies // Local dependencies
@ -10,6 +13,7 @@ use glib::{Bytes, Uri, UriHideFlags};
pub enum Request { pub enum Request {
Gemini { Gemini {
uri: Uri, uri: Uri,
mode: Mode,
}, },
Titan { Titan {
uri: Uri, uri: Uri,
@ -18,6 +22,7 @@ pub enum Request {
/// but server MAY reject the request without `mime` value provided. /// but server MAY reject the request without `mime` value provided.
mime: Option<String>, mime: Option<String>,
token: Option<String>, token: Option<String>,
mode: Mode,
}, },
} }
@ -27,12 +32,13 @@ impl Request {
/// Generate header string for `Self` /// Generate header string for `Self`
pub fn header(&self) -> String { pub fn header(&self) -> String {
match self { match self {
Self::Gemini { uri } => format!("{uri}\r\n"), Self::Gemini { uri, .. } => format!("{uri}\r\n"),
Self::Titan { Self::Titan {
uri, uri,
data, data,
mime, mime,
token, token,
..
} => { } => {
let mut header = format!( let mut header = format!(
"{};size={}", "{};size={}",
@ -57,7 +63,7 @@ impl Request {
/// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html) /// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html)
pub fn uri(&self) -> &Uri { pub fn uri(&self) -> &Uri {
match self { match self {
Self::Gemini { uri } => uri, Self::Gemini { uri, .. } => uri,
Self::Titan { uri, .. } => uri, Self::Titan { uri, .. } => uri,
} }
} }
@ -79,7 +85,8 @@ fn test_gemini_header() {
assert_eq!( assert_eq!(
Request::Gemini { Request::Gemini {
uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(),
mode: Mode::Header
} }
.header(), .header(),
format!("{REQUEST}\r\n") format!("{REQUEST}\r\n")
@ -103,7 +110,8 @@ fn test_titan_header() {
.unwrap(), .unwrap(),
data: Bytes::from(DATA), data: Bytes::from(DATA),
mime: Some(MIME.to_string()), mime: Some(MIME.to_string()),
token: Some(TOKEN.to_string()) token: Some(TOKEN.to_string()),
mode: Mode::Header
} }
.header(), .header(),
format!( format!(

View file

@ -0,0 +1,4 @@
pub enum Mode {
Header,
All,
}

View file

@ -6,7 +6,7 @@ pub mod redirect;
pub mod success; pub mod success;
pub use certificate::Certificate; pub use certificate::Certificate;
pub use error::Error; pub use error::{Error, HeaderBytesError};
pub use failure::Failure; pub use failure::Failure;
pub use input::Input; pub use input::Input;
pub use redirect::Redirect; pub use redirect::Redirect;
@ -29,13 +29,13 @@ pub enum Response {
impl Response { impl Response {
/// Asynchronously create new `Self` for given `Connection` /// Asynchronously create new `Self` for given `Connection`
pub fn from_connection_async( pub fn header_from_connection_async(
connection: Connection, connection: Connection,
priority: Priority, priority: Priority,
cancellable: Cancellable, cancellable: Cancellable,
callback: impl FnOnce(Result<Self, Error>, Connection) + 'static, callback: impl FnOnce(Result<Self, Error>, Connection) + 'static,
) { ) {
from_stream_async( header_from_stream_async(
Vec::with_capacity(HEADER_LEN), Vec::with_capacity(HEADER_LEN),
connection.stream(), connection.stream(),
cancellable, cancellable,
@ -44,12 +44,12 @@ impl Response {
callback( callback(
match result { match result {
Ok(buffer) => match buffer.first() { Ok(buffer) => match buffer.first() {
Some(byte) => match byte { Some(b) => match b {
b'1' => match Input::from_utf8(&buffer) { b'1' => match Input::from_utf8(&buffer) {
Ok(input) => Ok(Self::Input(input)), Ok(input) => Ok(Self::Input(input)),
Err(e) => Err(Error::Input(e)), Err(e) => Err(Error::Input(e)),
}, },
b'2' => match Success::from_utf8(&buffer) { b'2' => match Success::parse(&buffer) {
Ok(success) => Ok(Self::Success(success)), Ok(success) => Ok(Self::Success(success)),
Err(e) => Err(Error::Success(e)), Err(e) => Err(Error::Success(e)),
}, },
@ -65,9 +65,9 @@ impl Response {
Ok(certificate) => Ok(Self::Certificate(certificate)), Ok(certificate) => Ok(Self::Certificate(certificate)),
Err(e) => Err(Error::Certificate(e)), Err(e) => Err(Error::Certificate(e)),
}, },
_ => Err(Error::Code), b => Err(Error::Code(*b)),
}, },
None => Err(Error::Protocol), None => Err(Error::Protocol(buffer)),
}, },
Err(e) => Err(e), Err(e) => Err(e),
}, },
@ -84,43 +84,63 @@ impl Response {
/// ///
/// Return UTF-8 buffer collected /// Return UTF-8 buffer collected
/// * requires `IOStream` reference to keep `Connection` active in async thread /// * requires `IOStream` reference to keep `Connection` active in async thread
fn from_stream_async( fn header_from_stream_async(
mut buffer: Vec<u8>, mut buffer: Vec<u8>,
stream: impl IsA<IOStream>, stream: impl IsA<IOStream>,
cancellable: Cancellable, cancellable: Cancellable,
priority: Priority, priority: Priority,
on_complete: impl FnOnce(Result<Vec<u8>, Error>) + 'static, callback: impl FnOnce(Result<Vec<u8>, Error>) + 'static,
) { ) {
use gio::prelude::{IOStreamExt, InputStreamExtManual}; use gio::prelude::{IOStreamExt, InputStreamExtManual};
stream.input_stream().read_async( stream.input_stream().read_async(
vec![0], vec![0],
priority, priority,
Some(&cancellable.clone()), Some(&cancellable.clone()),
move |result| match result { move |result| match result {
Ok((mut bytes, size)) => { Ok((bytes, size)) => {
// Expect valid header length if size == 0 {
if size == 0 || buffer.len() >= HEADER_LEN { return callback(Ok(buffer));
return on_complete(Err(Error::Protocol));
} }
if buffer.len() + bytes.len() > HEADER_LEN {
// Read next byte without record buffer.extend(bytes);
if bytes.contains(&b'\r') { return callback(Err(Error::Protocol(buffer)));
return from_stream_async(buffer, stream, cancellable, priority, on_complete);
} }
if bytes[0] == b'\r' {
// Complete without record buffer.extend(bytes);
if bytes.contains(&b'\n') { return header_from_stream_async(
return on_complete(Ok(buffer)); buffer,
stream,
cancellable,
priority,
callback,
);
} }
if bytes[0] == b'\n' {
// Record buffer.extend(bytes);
buffer.append(&mut bytes); return callback(Ok(buffer));
}
// Continue buffer.extend(bytes);
from_stream_async(buffer, stream, cancellable, priority, on_complete); header_from_stream_async(buffer, stream, cancellable, priority, callback)
} }
Err((data, e)) => on_complete(Err(Error::Stream(e, data))), Err((data, e)) => callback(Err(Error::Stream(e, data))),
}, },
) )
} }
/// Get header bytes slice
/// * common for all child parsers
fn header_bytes(buffer: &[u8]) -> Result<&[u8], HeaderBytesError> {
for (i, b) in buffer.iter().enumerate() {
if i > 1024 {
return Err(HeaderBytesError::Len);
}
if *b == b'\r' {
let n = i + 1;
if buffer.get(n).is_some_and(|b| *b == b'\n') {
return Ok(&buffer[..n]);
}
break;
}
}
Err(HeaderBytesError::End)
}

View file

@ -6,10 +6,10 @@ use std::{
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Certificate(super::certificate::Error), Certificate(super::certificate::Error),
Code, Code(u8),
Failure(super::failure::Error), Failure(super::failure::Error),
Input(super::input::Error), Input(super::input::Error),
Protocol, Protocol(Vec<u8>),
Redirect(super::redirect::Error), Redirect(super::redirect::Error),
Stream(glib::Error, Vec<u8>), Stream(glib::Error, Vec<u8>),
Success(super::success::Error), Success(super::success::Error),
@ -22,8 +22,8 @@ impl Display for Error {
Self::Certificate(e) => { Self::Certificate(e) => {
write!(f, "Certificate error: {e}") write!(f, "Certificate error: {e}")
} }
Self::Code => { Self::Code(b) => {
write!(f, "Code group error") write!(f, "Unexpected status code byte: {b}")
} }
Self::Failure(e) => { Self::Failure(e) => {
write!(f, "Failure error: {e}") write!(f, "Failure error: {e}")
@ -31,7 +31,7 @@ impl Display for Error {
Self::Input(e) => { Self::Input(e) => {
write!(f, "Input error: {e}") write!(f, "Input error: {e}")
} }
Self::Protocol => { Self::Protocol(..) => {
write!(f, "Protocol error") write!(f, "Protocol error")
} }
Self::Redirect(e) => { Self::Redirect(e) => {
@ -49,3 +49,22 @@ impl Display for Error {
} }
} }
} }
#[derive(Debug)]
pub enum HeaderBytesError {
Len,
End,
}
impl Display for HeaderBytesError {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Len => {
write!(f, "Unexpected header length")
}
Self::End => {
write!(f, "Unexpected header end")
}
}
}
}

View file

@ -1,89 +1,33 @@
pub mod default;
pub mod error; pub mod error;
pub use default::Default;
pub use error::Error; pub use error::Error;
const DEFAULT: (u8, &str) = (20, "Success"); pub const CODE: u8 = b'2';
pub enum Success { pub enum Success {
Default { mime: String }, Default(Default),
// reserved for 2* codes // reserved for 2* codes
} }
impl Success { impl Success {
// Constructors // Constructors
/// Create new `Self` from buffer include header bytes /// Parse new `Self` from buffer bytes
pub fn from_utf8(buffer: &[u8]) -> Result<Self, Error> { pub fn parse(buffer: &[u8]) -> Result<Self, Error> {
use std::str::FromStr; if !buffer.first().is_some_and(|b| *b == CODE) {
match std::str::from_utf8(buffer) { return Err(Error::Code);
Ok(header) => Self::from_str(header),
Err(e) => Err(Error::Utf8Error(e)),
} }
} match Default::parse(&buffer) {
Ok(default) => Ok(Self::Default(default)),
// Convertors Err(e) => Err(Error::Default(e)),
pub fn to_code(&self) -> u8 {
match self {
Self::Default { .. } => DEFAULT.0,
}
}
// Getters
pub fn mime(&self) -> &str {
match self {
Self::Default { mime } => mime,
}
}
}
impl std::fmt::Display for Success {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Default { .. } => DEFAULT.1,
}
)
}
}
impl std::str::FromStr for Success {
type Err = Error;
fn from_str(header: &str) -> Result<Self, Self::Err> {
use glib::{Regex, RegexCompileFlags, RegexMatchFlags};
match Regex::split_simple(
r"^20\s([^\/]+\/[^\s;]+)",
header,
RegexCompileFlags::DEFAULT,
RegexMatchFlags::DEFAULT,
)
.get(1)
{
Some(mime) => {
let mime = mime.trim();
if mime.is_empty() {
Err(Error::Mime)
} else {
Ok(Self::Default {
mime: mime.to_lowercase(),
})
}
}
None => Err(Error::Protocol),
} }
} }
} }
#[test] #[test]
fn test_from_str() { fn test() {
use std::str::FromStr; // let default = Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes());
todo!()
let default = Success::from_str("20 text/gemini; charset=utf-8; lang=en\r\n").unwrap();
assert_eq!(default.mime(), "text/gemini");
assert_eq!(default.to_code(), DEFAULT.0);
assert_eq!(default.to_string(), DEFAULT.1);
} }

View file

@ -0,0 +1,27 @@
pub mod error;
pub mod header;
pub use error::Error;
pub use header::Header;
pub const CODE: &[u8] = b"20";
pub struct Default {
pub header: Header,
pub content: Option<Vec<u8>>,
}
impl Default {
// Constructors
pub fn parse(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(CODE) {
return Err(Error::Code);
}
let header = Header::parse(buffer).map_err(|e| Error::Header(e))?;
Ok(Self {
content: buffer.get(header.len() + 1..).map(|v| v.to_vec()),
header,
})
}
}

View file

@ -0,0 +1,20 @@
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Code,
Header(super::header::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
}
}
}

View file

@ -0,0 +1,43 @@
pub mod error;
pub use error::Error;
pub struct Header(Vec<u8>);
impl Header {
// Constructors
pub fn parse(buffer: &[u8]) -> Result<Self, Error> {
if !buffer.starts_with(super::CODE) {
return Err(Error::Code);
}
Ok(Self(
crate::client::connection::response::header_bytes(buffer)
.map_err(|e| Error::Header(e))?
.to_vec(),
))
}
// Getters
/// Parse content type for `Self`
pub fn mime(&self) -> Result<String, Error> {
glib::Regex::split_simple(
r"^\d{2}\s([^\/]+\/[^\s;]+)",
std::str::from_utf8(&self.0).map_err(|e| Error::Utf8Error(e))?,
glib::RegexCompileFlags::DEFAULT,
glib::RegexMatchFlags::DEFAULT,
)
.get(1)
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map_or(Err(Error::Mime), |s| Ok(s.to_lowercase()))
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}

View file

@ -0,0 +1,31 @@
use std::{
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
#[derive(Debug)]
pub enum Error {
Code,
Mime,
Header(crate::client::connection::response::HeaderBytesError),
Utf8Error(Utf8Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Code => {
write!(f, "Unexpected status code")
}
Self::Mime => {
write!(f, "Unexpected content type")
}
Self::Header(e) => {
write!(f, "Header error: {e}")
}
Self::Utf8Error(e) => {
write!(f, "UTF-8 error: {e}")
}
}
}
}

View file

@ -1,26 +1,19 @@
use std::{ use std::fmt::{Display, Formatter, Result};
fmt::{Display, Formatter, Result},
str::Utf8Error,
};
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Protocol, Code,
Mime, Default(super::default::Error),
Utf8Error(Utf8Error),
} }
impl Display for Error { impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result { fn fmt(&self, f: &mut Formatter) -> Result {
match self { match self {
Self::Utf8Error(e) => { Self::Code => {
write!(f, "UTF-8 error: {e}") write!(f, "Unexpected status code")
} }
Self::Protocol => { Self::Default(e) => {
write!(f, "Protocol error") write!(f, "Header error: {e}")
}
Self::Mime => {
write!(f, "MIME error")
} }
} }
} }