From 7c518cecf6be56d8b917b10c5131b0ef7dcf3377 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 06:50:08 +0200 Subject: [PATCH] begin header holder implementation with lazy parser by getters, add request::Mode, add common header_bytes helper --- Cargo.toml | 2 +- README.md | 3 +- src/client/connection.rs | 58 +++++++------ src/client/connection/request.rs | 16 +++- src/client/connection/request/mode.rs | 4 + src/client/connection/response.rs | 78 ++++++++++------- src/client/connection/response/error.rs | 29 +++++-- src/client/connection/response/success.rs | 86 ++++--------------- .../connection/response/success/default.rs | 27 ++++++ .../response/success/default/error.rs | 20 +++++ .../response/success/default/header.rs | 43 ++++++++++ .../response/success/default/header/error.rs | 31 +++++++ .../connection/response/success/error.rs | 21 ++--- 13 files changed, 267 insertions(+), 151 deletions(-) create mode 100644 src/client/connection/request/mode.rs create mode 100644 src/client/connection/response/success/default.rs create mode 100644 src/client/connection/response/success/default/error.rs create mode 100644 src/client/connection/response/success/default/header.rs create mode 100644 src/client/connection/response/success/default/header/error.rs diff --git a/Cargo.toml b/Cargo.toml index 54dd4b6..442d0ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.3" +version = "0.18.0" edition = "2024" license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index cd21d88..c9e3c23 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ use gio::*; use glib::*; use ggemini::client::{ - connection::{Request, Response}, + connection::{request::{Mode, Request}, Response}, Client, }; @@ -51,6 +51,7 @@ fn main() -> ExitCode { Client::new().request_async( Request::Gemini { // or `Request::Titan` uri: Uri::parse("gemini://geminiprotocol.net/", UriFlags::NONE).unwrap(), + mode: Mode::Header // handle content separately (based on MIME) }, Priority::DEFAULT, Cancellable::new(), diff --git a/src/client/connection.rs b/src/client/connection.rs index 028e00e..8bea8b2 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -3,7 +3,7 @@ pub mod request; pub mod response; pub use error::Error; -pub use request::Request; +pub use request::{Mode, Request}; pub use response::Response; // Local dependencies @@ -74,36 +74,42 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match request { - Request::Gemini { .. } => Response::from_connection_async( - self, - priority, - cancellable, - |result, connection| { - callback(match result { - Ok(response) => Ok((response, connection)), - Err(e) => Err(Error::Response(e)), - }) - }, - ), + Request::Gemini { mode, .. } => match mode { + Mode::All => todo!(), + Mode::Header => Response::header_from_connection_async( + self, + priority, + cancellable, + |result, connection| { + callback(match result { + Ok(response) => Ok((response, connection)), + Err(e) => Err(Error::Response(e)), + }) + }, + ), + }, // Make sure **all data bytes** sent to the destination // > 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 - Request::Titan { data, .. } => output_stream.write_all_async( + Request::Titan { data, mode, .. } => output_stream.write_all_async( data, priority, Some(&cancellable.clone()), move |result| match result { - Ok(_) => Response::from_connection_async( - self, - priority, - cancellable, - |result, connection| { - callback(match result { - Ok(response) => Ok((response, connection)), - Err(e) => Err(Error::Response(e)), - }) - }, - ), + Ok(_) => match mode { + Mode::All => todo!(), + Mode::Header => Response::header_from_connection_async( + self, + priority, + cancellable, + |result, connection| { + callback(match result { + Ok(response) => Ok((response, connection)), + Err(e) => Err(Error::Response(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) /// 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) -pub fn new_tls_client_connection( +fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, is_session_resumption: bool, diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 75a4927..83632ca 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -1,5 +1,8 @@ pub mod error; +pub mod mode; + pub use error::Error; +pub use mode::Mode; // Local dependencies @@ -10,6 +13,7 @@ use glib::{Bytes, Uri, UriHideFlags}; pub enum Request { Gemini { uri: Uri, + mode: Mode, }, Titan { uri: Uri, @@ -18,6 +22,7 @@ pub enum Request { /// but server MAY reject the request without `mime` value provided. mime: Option, token: Option, + mode: Mode, }, } @@ -27,12 +32,13 @@ impl Request { /// Generate header string for `Self` pub fn header(&self) -> String { match self { - Self::Gemini { uri } => format!("{uri}\r\n"), + Self::Gemini { uri, .. } => format!("{uri}\r\n"), Self::Titan { uri, data, mime, token, + .. } => { let mut header = format!( "{};size={}", @@ -57,7 +63,7 @@ impl Request { /// Get reference to `Self` [Uri](https://docs.gtk.org/glib/struct.Uri.html) pub fn uri(&self) -> &Uri { match self { - Self::Gemini { uri } => uri, + Self::Gemini { uri, .. } => uri, Self::Titan { uri, .. } => uri, } } @@ -79,7 +85,8 @@ fn test_gemini_header() { assert_eq!( Request::Gemini { - uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap() + uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(), + mode: Mode::Header } .header(), format!("{REQUEST}\r\n") @@ -103,7 +110,8 @@ fn test_titan_header() { .unwrap(), data: Bytes::from(DATA), mime: Some(MIME.to_string()), - token: Some(TOKEN.to_string()) + token: Some(TOKEN.to_string()), + mode: Mode::Header } .header(), format!( diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs new file mode 100644 index 0000000..6713bbd --- /dev/null +++ b/src/client/connection/request/mode.rs @@ -0,0 +1,4 @@ +pub enum Mode { + Header, + All, +} diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 9a16679..98fe2e3 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -6,7 +6,7 @@ pub mod redirect; pub mod success; pub use certificate::Certificate; -pub use error::Error; +pub use error::{Error, HeaderBytesError}; pub use failure::Failure; pub use input::Input; pub use redirect::Redirect; @@ -29,13 +29,13 @@ pub enum Response { impl Response { /// Asynchronously create new `Self` for given `Connection` - pub fn from_connection_async( + pub fn header_from_connection_async( connection: Connection, priority: Priority, cancellable: Cancellable, callback: impl FnOnce(Result, Connection) + 'static, ) { - from_stream_async( + header_from_stream_async( Vec::with_capacity(HEADER_LEN), connection.stream(), cancellable, @@ -44,12 +44,12 @@ impl Response { callback( match result { Ok(buffer) => match buffer.first() { - Some(byte) => match byte { + Some(b) => match b { b'1' => match Input::from_utf8(&buffer) { Ok(input) => Ok(Self::Input(input)), 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)), Err(e) => Err(Error::Success(e)), }, @@ -65,9 +65,9 @@ impl Response { Ok(certificate) => Ok(Self::Certificate(certificate)), 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), }, @@ -84,43 +84,63 @@ impl Response { /// /// Return UTF-8 buffer collected /// * requires `IOStream` reference to keep `Connection` active in async thread -fn from_stream_async( +fn header_from_stream_async( mut buffer: Vec, stream: impl IsA, cancellable: Cancellable, priority: Priority, - on_complete: impl FnOnce(Result, Error>) + 'static, + callback: impl FnOnce(Result, Error>) + 'static, ) { use gio::prelude::{IOStreamExt, InputStreamExtManual}; - stream.input_stream().read_async( vec![0], priority, Some(&cancellable.clone()), move |result| match result { - Ok((mut bytes, size)) => { - // Expect valid header length - if size == 0 || buffer.len() >= HEADER_LEN { - return on_complete(Err(Error::Protocol)); + Ok((bytes, size)) => { + if size == 0 { + return callback(Ok(buffer)); } - - // Read next byte without record - if bytes.contains(&b'\r') { - return from_stream_async(buffer, stream, cancellable, priority, on_complete); + if buffer.len() + bytes.len() > HEADER_LEN { + buffer.extend(bytes); + return callback(Err(Error::Protocol(buffer))); } - - // Complete without record - if bytes.contains(&b'\n') { - return on_complete(Ok(buffer)); + if bytes[0] == b'\r' { + buffer.extend(bytes); + return header_from_stream_async( + buffer, + stream, + cancellable, + priority, + callback, + ); } - - // Record - buffer.append(&mut bytes); - - // Continue - from_stream_async(buffer, stream, cancellable, priority, on_complete); + if bytes[0] == b'\n' { + buffer.extend(bytes); + return callback(Ok(buffer)); + } + buffer.extend(bytes); + 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) +} diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index df8cda4..022ed62 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -6,10 +6,10 @@ use std::{ #[derive(Debug)] pub enum Error { Certificate(super::certificate::Error), - Code, + Code(u8), Failure(super::failure::Error), Input(super::input::Error), - Protocol, + Protocol(Vec), Redirect(super::redirect::Error), Stream(glib::Error, Vec), Success(super::success::Error), @@ -22,8 +22,8 @@ impl Display for Error { Self::Certificate(e) => { write!(f, "Certificate error: {e}") } - Self::Code => { - write!(f, "Code group error") + Self::Code(b) => { + write!(f, "Unexpected status code byte: {b}") } Self::Failure(e) => { write!(f, "Failure error: {e}") @@ -31,7 +31,7 @@ impl Display for Error { Self::Input(e) => { write!(f, "Input error: {e}") } - Self::Protocol => { + Self::Protocol(..) => { write!(f, "Protocol error") } 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") + } + } + } +} diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index e5ad6f4..591510d 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -1,89 +1,33 @@ +pub mod default; pub mod error; + +pub use default::Default; pub use error::Error; -const DEFAULT: (u8, &str) = (20, "Success"); +pub const CODE: u8 = b'2'; pub enum Success { - Default { mime: String }, + Default(Default), // reserved for 2* codes } impl Success { // Constructors - /// Create new `Self` from buffer include header bytes - pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + /// Parse new `Self` from buffer bytes + pub fn parse(buffer: &[u8]) -> Result { + if !buffer.first().is_some_and(|b| *b == CODE) { + return Err(Error::Code); } - } - - // Convertors - - 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 { - 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), + match Default::parse(&buffer) { + Ok(default) => Ok(Self::Default(default)), + Err(e) => Err(Error::Default(e)), } } } #[test] -fn test_from_str() { - use std::str::FromStr; - - 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); +fn test() { + // let default = Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()); + todo!() } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs new file mode 100644 index 0000000..34f9cb0 --- /dev/null +++ b/src/client/connection/response/success/default.rs @@ -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>, +} + +impl Default { + // Constructors + + pub fn parse(buffer: &[u8]) -> Result { + 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, + }) + } +} diff --git a/src/client/connection/response/success/default/error.rs b/src/client/connection/response/success/default/error.rs new file mode 100644 index 0000000..d5b28b5 --- /dev/null +++ b/src/client/connection/response/success/default/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs new file mode 100644 index 0000000..ef326bc --- /dev/null +++ b/src/client/connection/response/success/default/header.rs @@ -0,0 +1,43 @@ +pub mod error; +pub use error::Error; + +pub struct Header(Vec); + +impl Header { + // Constructors + + pub fn parse(buffer: &[u8]) -> Result { + 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 { + 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 + } +} diff --git a/src/client/connection/response/success/default/header/error.rs b/src/client/connection/response/success/default/header/error.rs new file mode 100644 index 0000000..4daca3a --- /dev/null +++ b/src/client/connection/response/success/default/header/error.rs @@ -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}") + } + } + } +} diff --git a/src/client/connection/response/success/error.rs b/src/client/connection/response/success/error.rs index 2dbe363..fe32c5f 100644 --- a/src/client/connection/response/success/error.rs +++ b/src/client/connection/response/success/error.rs @@ -1,26 +1,19 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Protocol, - Mime, - Utf8Error(Utf8Error), + Code, + Default(super::default::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Code => { + write!(f, "Unexpected status code") } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Mime => { - write!(f, "MIME error") + Self::Default(e) => { + write!(f, "Header error: {e}") } } }