From eee87d66b4011dbf58272ccc08568d3dbaa21b0a Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 28 Jan 2025 10:19:24 +0200 Subject: [PATCH 01/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b19da7b..52cb044 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.14.0" +version = "0.14.1" edition = "2021" license = "MIT" readme = "README.md" From cdac038135294b1d6e2bf2ebad52241f264b8e0f Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 28 Jan 2025 11:58:35 +0200 Subject: [PATCH 02/95] fix example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 91e7552..23ee9de 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ fn main() -> ExitCode { // route by status code match response.meta.status { // code 20, handle `GIOStream` by content type - Status::Success => match response.meta.mime.unwrap().value.as_str() { + Status::Success => match response.meta.mime.unwrap().as_str() { // gemtext, see ggemtext crate to parse "text/gemini" => todo!(), // other content types From 5358e43697f50e2b9062f00bb7aa2cf95d5373ac Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 2 Feb 2025 22:12:40 +0200 Subject: [PATCH 03/95] update `Response` API --- Cargo.toml | 2 +- src/client/connection/response.rs | 125 +++++++++++-- src/client/connection/response/certificate.rs | 113 ++++++++++++ .../connection/response/certificate/error.rs | 23 +++ src/client/connection/response/error.rs | 45 ++++- src/client/connection/response/failure.rs | 54 ++++++ .../connection/response/failure/error.rs | 28 +++ .../connection/response/failure/permanent.rs | 169 ++++++++++++++++++ .../response/failure/permanent/error.rs | 23 +++ .../connection/response/failure/temporary.rs | 169 ++++++++++++++++++ .../response/failure/temporary/error.rs | 23 +++ src/client/connection/response/input.rs | 109 +++++++++++ .../response/{meta/data => input}/error.rs | 11 +- src/client/connection/response/meta.rs | 147 --------------- .../connection/response/meta/charset.rs | 1 - src/client/connection/response/meta/data.rs | 82 --------- src/client/connection/response/meta/error.rs | 33 ---- .../connection/response/meta/language.rs | 1 - src/client/connection/response/meta/mime.rs | 70 -------- .../connection/response/meta/mime/error.rs | 22 --- src/client/connection/response/meta/status.rs | 109 ----------- src/client/connection/response/redirect.rs | 112 ++++++++++++ .../connection/response/redirect/error.rs | 27 +++ src/client/connection/response/success.rs | 89 +++++++++ .../{meta/status => success}/error.rs | 17 +- 25 files changed, 1102 insertions(+), 502 deletions(-) create mode 100644 src/client/connection/response/certificate.rs create mode 100644 src/client/connection/response/certificate/error.rs create mode 100644 src/client/connection/response/failure.rs create mode 100644 src/client/connection/response/failure/error.rs create mode 100644 src/client/connection/response/failure/permanent.rs create mode 100644 src/client/connection/response/failure/permanent/error.rs create mode 100644 src/client/connection/response/failure/temporary.rs create mode 100644 src/client/connection/response/failure/temporary/error.rs create mode 100644 src/client/connection/response/input.rs rename src/client/connection/response/{meta/data => input}/error.rs (59%) delete mode 100644 src/client/connection/response/meta.rs delete mode 100644 src/client/connection/response/meta/charset.rs delete mode 100644 src/client/connection/response/meta/data.rs delete mode 100644 src/client/connection/response/meta/error.rs delete mode 100644 src/client/connection/response/meta/language.rs delete mode 100644 src/client/connection/response/meta/mime.rs delete mode 100644 src/client/connection/response/meta/mime/error.rs delete mode 100644 src/client/connection/response/meta/status.rs create mode 100644 src/client/connection/response/redirect.rs create mode 100644 src/client/connection/response/redirect/error.rs create mode 100644 src/client/connection/response/success.rs rename src/client/connection/response/{meta/status => success}/error.rs (52%) diff --git a/Cargo.toml b/Cargo.toml index 52cb044..f54e4c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.14.1" +version = "0.15.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 3ede7d0..9f2b901 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -1,37 +1,126 @@ //! Read and parse Gemini response as Object -pub mod data; +pub mod certificate; +pub mod data; // @TODO deprecated pub mod error; -pub mod meta; +pub mod failure; +pub mod input; +pub mod redirect; +pub mod success; +pub use certificate::Certificate; pub use error::Error; -pub use meta::Meta; +pub use failure::Failure; +pub use input::Input; +pub use redirect::Redirect; +pub use success::Success; use super::Connection; -use gio::Cancellable; -use glib::Priority; +use gio::{Cancellable, IOStream}; +use glib::{object::IsA, Priority}; -pub struct Response { - pub connection: Connection, - pub meta: Meta, +const HEADER_LEN: usize = 0x400; // 1024 + +/// https://geminiprotocol.net/docs/protocol-specification.gmi#responses +pub enum Response { + Input(Input), // 1* + Success(Success), // 2* + Redirect(Redirect), // 3* + Failure(Failure), // 4*,5* + Certificate(Certificate), // 6* } impl Response { - // Constructors - - /// Create new `Self` from given `Connection` - /// * useful for manual [IOStream](https://docs.gtk.org/gio/class.IOStream.html) handle (based on `Meta` bytes pre-parsed) + /// Asynchronously create new `Self` for given `Connection` pub fn from_connection_async( connection: Connection, priority: Priority, cancellable: Cancellable, callback: impl FnOnce(Result) + 'static, ) { - Meta::from_stream_async(connection.stream(), priority, cancellable, |result| { - callback(match result { - Ok(meta) => Ok(Self { connection, meta }), - Err(e) => Err(Error::Meta(e)), - }) - }) + from_stream_async( + Vec::with_capacity(HEADER_LEN), + connection.stream(), + cancellable, + priority, + |result| { + callback(match result { + Ok(buffer) => match buffer.first() { + Some(byte) => match byte { + 1 => match Input::from_utf8(&buffer) { + Ok(input) => Ok(Self::Input(input)), + Err(e) => Err(Error::Input(e)), + }, + 2 => match Success::from_utf8(&buffer) { + Ok(success) => Ok(Self::Success(success)), + Err(e) => Err(Error::Success(e)), + }, + 3 => match Redirect::from_utf8(&buffer) { + Ok(redirect) => Ok(Self::Redirect(redirect)), + Err(e) => Err(Error::Redirect(e)), + }, + 4 | 5 => match Failure::from_utf8(&buffer) { + Ok(failure) => Ok(Self::Failure(failure)), + Err(e) => Err(Error::Failure(e)), + }, + 6 => match Certificate::from_utf8(&buffer) { + Ok(certificate) => Ok(Self::Certificate(certificate)), + Err(e) => Err(Error::Certificate(e)), + }, + b => Err(Error::Code(*b)), + }, + None => Err(Error::Protocol), + }, + Err(e) => Err(e), + }) + }, + ); } } + +// Tools + +/// Asynchronously read header bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) +/// +/// Return UTF-8 buffer collected +/// * requires `IOStream` reference to keep `Connection` active in async thread +fn from_stream_async( + mut buffer: Vec, + stream: impl IsA, + cancellable: Cancellable, + priority: Priority, + on_complete: 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)); + } + + // Read next byte without record + if bytes.contains(&b'\r') { + return from_stream_async(buffer, stream, cancellable, priority, on_complete); + } + + // Complete without record + if bytes.contains(&b'\n') { + return on_complete(Ok(buffer)); + } + + // Record + buffer.append(&mut bytes); + + // Continue + from_stream_async(buffer, stream, cancellable, priority, on_complete); + } + Err((data, e)) => on_complete(Err(Error::Stream(e, data))), + }, + ) +} diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs new file mode 100644 index 0000000..450d6d7 --- /dev/null +++ b/src/client/connection/response/certificate.rs @@ -0,0 +1,113 @@ +pub mod error; +pub use error::Error; + +const REQUIRED: (u8, &str) = (10, "Certificate required"); +const NOT_AUTHORIZED: (u8, &str) = (11, "Certificate not authorized"); +const NOT_VALID: (u8, &str) = (11, "Certificate not valid"); + +/// 6* status code group +/// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates +pub enum Certificate { + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 + Required { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized + NotAuthorized { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid + NotValid { message: Option }, +} + +impl Certificate { + // 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)), + } + } + + // Getters + + pub fn to_code(&self) -> u8 { + match self { + Self::Required { .. } => REQUIRED, + Self::NotAuthorized { .. } => NOT_AUTHORIZED, + Self::NotValid { .. } => NOT_VALID, + } + .0 + } + + pub fn message(&self) -> Option<&str> { + match self { + Self::Required { message } => message, + Self::NotAuthorized { message } => message, + Self::NotValid { message } => message, + } + .as_deref() + } +} + +impl std::fmt::Display for Certificate { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Required { message } => message.as_deref().unwrap_or(REQUIRED.1), + Self::NotAuthorized { message } => message.as_deref().unwrap_or(NOT_AUTHORIZED.1), + Self::NotValid { message } => message.as_deref().unwrap_or(NOT_VALID.1), + } + ) + } +} + +impl std::str::FromStr for Certificate { + type Err = Error; + fn from_str(header: &str) -> Result { + if let Some(postfix) = header.strip_prefix("60") { + return Ok(Self::Required { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("61") { + return Ok(Self::NotAuthorized { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("62") { + return Ok(Self::NotValid { + message: message(postfix), + }); + } + Err(Error::Code) + } +} + +// Tools + +fn message(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + let required = Certificate::from_str("60 Message\r\n").unwrap(); + + assert_eq!(required.message(), Some("Message")); + assert_eq!(required.to_code(), REQUIRED.0); + + let required = Certificate::from_str("60\r\n").unwrap(); + + assert_eq!(required.message(), None); + assert_eq!(required.to_code(), REQUIRED.0); + assert_eq!(required.to_string(), REQUIRED.1); +} diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/certificate/error.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Code, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Status code error") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + } + } +} diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index 9834190..99895d9 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -1,19 +1,50 @@ -use std::fmt::{Display, Formatter, Result}; +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; #[derive(Debug)] pub enum Error { - Meta(super::meta::Error), - Stream, + Certificate(super::certificate::Error), + Code(u8), + Failure(super::failure::Error), + Input(super::input::Error), + Protocol, + Redirect(super::redirect::Error), + Stream(glib::Error, Vec), + Success(super::success::Error), + Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Meta(e) => { - write!(f, "Meta read error: {e}") + Self::Certificate(e) => { + write!(f, "Certificate error: {e}") } - Self::Stream => { - write!(f, "I/O stream error") + Self::Code(e) => { + write!(f, "Code group error: {e}*") + } + Self::Failure(e) => { + write!(f, "Failure error: {e}") + } + Self::Input(e) => { + write!(f, "Input error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Redirect(e) => { + write!(f, "Redirect error: {e}") + } + Self::Stream(e, ..) => { + write!(f, "I/O stream error: {e}") + } + Self::Success(e) => { + write!(f, "Success error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") } } } diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs new file mode 100644 index 0000000..c0e76ec --- /dev/null +++ b/src/client/connection/response/failure.rs @@ -0,0 +1,54 @@ +pub mod error; +pub mod permanent; +pub mod temporary; + +pub use error::Error; +pub use permanent::Permanent; +pub use temporary::Temporary; + +pub enum Failure { + /// 4* status code group + /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure + Temporary(Temporary), + /// 5* status code group + /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure + Permanent(Permanent), +} + +impl Failure { + // Constructors + + /// Create new `Self` from buffer include header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + match buffer.first() { + Some(byte) => match byte { + 4 => match Temporary::from_utf8(buffer) { + Ok(input) => Ok(Self::Temporary(input)), + Err(e) => Err(Error::Temporary(e)), + }, + 5 => match Permanent::from_utf8(buffer) { + Ok(failure) => Ok(Self::Permanent(failure)), + Err(e) => Err(Error::Permanent(e)), + }, + b => Err(Error::Code(*b)), + }, + None => Err(Error::Protocol), + } + } + + // Getters + + pub fn to_code(&self) -> u8 { + match self { + Self::Permanent(permanent) => permanent.to_code(), + Self::Temporary(temporary) => temporary.to_code(), + } + } + + pub fn message(&self) -> Option<&str> { + match self { + Self::Permanent(permanent) => permanent.message(), + Self::Temporary(temporary) => temporary.message(), + } + } +} diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs new file mode 100644 index 0000000..60724eb --- /dev/null +++ b/src/client/connection/response/failure/error.rs @@ -0,0 +1,28 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code(u8), + Permanent(super::permanent::Error), + Protocol, + Temporary(super::temporary::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code(e) => { + write!(f, "Code group error: {e}*") + } + Self::Permanent(e) => { + write!(f, "Permanent failure group error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Temporary(e) => { + write!(f, "Temporary failure group error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs new file mode 100644 index 0000000..08b6c56 --- /dev/null +++ b/src/client/connection/response/failure/permanent.rs @@ -0,0 +1,169 @@ +pub mod error; +pub use error::Error; + +const DEFAULT: (u8, &str) = (50, "Unspecified"); +const NOT_FOUND: (u8, &str) = (51, "Not found"); +const GONE: (u8, &str) = (52, "Gone"); +const PROXY_REQUEST_REFUSED: (u8, &str) = (53, "Proxy request refused"); +const BAD_REQUEST: (u8, &str) = (59, "bad-request"); + +/// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure +pub enum Permanent { + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 + Default { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found + NotFound { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone + Gone { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused + ProxyRequestRefused { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request + BadRequest { message: Option }, +} + +impl Permanent { + // 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)), + } + } + + // Getters + + pub fn to_code(&self) -> u8 { + match self { + Self::Default { .. } => DEFAULT, + Self::NotFound { .. } => NOT_FOUND, + Self::Gone { .. } => GONE, + Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, + Self::BadRequest { .. } => BAD_REQUEST, + } + .0 + } + + pub fn message(&self) -> Option<&str> { + match self { + Self::Default { message } => message, + Self::NotFound { message } => message, + Self::Gone { message } => message, + Self::ProxyRequestRefused { message } => message, + Self::BadRequest { message } => message, + } + .as_deref() + } +} + +impl std::fmt::Display for Permanent { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), + Self::NotFound { message } => message.as_deref().unwrap_or(NOT_FOUND.1), + Self::Gone { message } => message.as_deref().unwrap_or(GONE.1), + Self::ProxyRequestRefused { message } => + message.as_deref().unwrap_or(PROXY_REQUEST_REFUSED.1), + Self::BadRequest { message } => message.as_deref().unwrap_or(BAD_REQUEST.1), + } + ) + } +} + +impl std::str::FromStr for Permanent { + type Err = Error; + fn from_str(header: &str) -> Result { + if let Some(postfix) = header.strip_prefix("50") { + return Ok(Self::Default { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("51") { + return Ok(Self::NotFound { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("52") { + return Ok(Self::Gone { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("53") { + return Ok(Self::ProxyRequestRefused { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("59") { + return Ok(Self::BadRequest { + message: message(postfix), + }); + } + Err(Error::Code) + } +} + +// Tools + +fn message(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + // 50 + let default = Permanent::from_str("50 Message\r\n").unwrap(); + assert_eq!(default.message(), Some("Message")); + assert_eq!(default.to_code(), DEFAULT.0); + + let default = Permanent::from_str("50\r\n").unwrap(); + assert_eq!(default.message(), None); + assert_eq!(default.to_code(), DEFAULT.0); + + // 51 + let not_found = Permanent::from_str("51 Message\r\n").unwrap(); + assert_eq!(not_found.message(), Some("Message")); + assert_eq!(not_found.to_code(), NOT_FOUND.0); + + let not_found = Permanent::from_str("51\r\n").unwrap(); + assert_eq!(not_found.message(), None); + assert_eq!(not_found.to_code(), NOT_FOUND.0); + + // 52 + let gone = Permanent::from_str("52 Message\r\n").unwrap(); + assert_eq!(gone.message(), Some("Message")); + assert_eq!(gone.to_code(), GONE.0); + + let gone = Permanent::from_str("52\r\n").unwrap(); + assert_eq!(gone.message(), None); + assert_eq!(gone.to_code(), GONE.0); + + // 53 + let proxy_request_refused = Permanent::from_str("53 Message\r\n").unwrap(); + assert_eq!(proxy_request_refused.message(), Some("Message")); + assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); + + let proxy_request_refused = Permanent::from_str("53\r\n").unwrap(); + assert_eq!(proxy_request_refused.message(), None); + assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); + + // 59 + let bad_request = Permanent::from_str("59 Message\r\n").unwrap(); + assert_eq!(bad_request.message(), Some("Message")); + assert_eq!(bad_request.to_code(), BAD_REQUEST.0); + + let bad_request = Permanent::from_str("59\r\n").unwrap(); + assert_eq!(bad_request.message(), None); + assert_eq!(bad_request.to_code(), BAD_REQUEST.0); +} diff --git a/src/client/connection/response/failure/permanent/error.rs b/src/client/connection/response/failure/permanent/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/failure/permanent/error.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Code, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Status code error") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs new file mode 100644 index 0000000..363645e --- /dev/null +++ b/src/client/connection/response/failure/temporary.rs @@ -0,0 +1,169 @@ +pub mod error; +pub use error::Error; + +const DEFAULT: (u8, &str) = (40, "Unspecified"); +const SERVER_UNAVAILABLE: (u8, &str) = (41, "Server unavailable"); +const CGI_ERROR: (u8, &str) = (42, "CGI error"); +const PROXY_ERROR: (u8, &str) = (43, "Proxy error"); +const SLOW_DOWN: (u8, &str) = (44, "Slow down"); + +/// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure +pub enum Temporary { + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 + Default { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable + ServerUnavailable { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error + CgiError { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error + ProxyError { message: Option }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down + SlowDown { message: Option }, +} + +impl Temporary { + // 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)), + } + } + + // Getters + + pub fn to_code(&self) -> u8 { + match self { + Self::Default { .. } => DEFAULT, + Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, + Self::CgiError { .. } => CGI_ERROR, + Self::ProxyError { .. } => PROXY_ERROR, + Self::SlowDown { .. } => SLOW_DOWN, + } + .0 + } + + pub fn message(&self) -> Option<&str> { + match self { + Self::Default { message } => message, + Self::ServerUnavailable { message } => message, + Self::CgiError { message } => message, + Self::ProxyError { message } => message, + Self::SlowDown { message } => message, + } + .as_deref() + } +} + +impl std::fmt::Display for Temporary { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), + Self::ServerUnavailable { message } => + message.as_deref().unwrap_or(SERVER_UNAVAILABLE.1), + Self::CgiError { message } => message.as_deref().unwrap_or(CGI_ERROR.1), + Self::ProxyError { message } => message.as_deref().unwrap_or(PROXY_ERROR.1), + Self::SlowDown { message } => message.as_deref().unwrap_or(SLOW_DOWN.1), + } + ) + } +} + +impl std::str::FromStr for Temporary { + type Err = Error; + fn from_str(header: &str) -> Result { + if let Some(postfix) = header.strip_prefix("40") { + return Ok(Self::Default { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("41") { + return Ok(Self::ServerUnavailable { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("42") { + return Ok(Self::CgiError { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("43") { + return Ok(Self::ProxyError { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("44") { + return Ok(Self::SlowDown { + message: message(postfix), + }); + } + Err(Error::Code) + } +} + +// Tools + +fn message(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + // 40 + let default = Temporary::from_str("40 Message\r\n").unwrap(); + assert_eq!(default.message(), Some("Message")); + assert_eq!(default.to_code(), DEFAULT.0); + + let default = Temporary::from_str("40\r\n").unwrap(); + assert_eq!(default.message(), None); + assert_eq!(default.to_code(), DEFAULT.0); + + // 41 + let server_unavailable = Temporary::from_str("41 Message\r\n").unwrap(); + assert_eq!(server_unavailable.message(), Some("Message")); + assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); + + let server_unavailable = Temporary::from_str("41\r\n").unwrap(); + assert_eq!(server_unavailable.message(), None); + assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); + + // 42 + let cgi_error = Temporary::from_str("42 Message\r\n").unwrap(); + assert_eq!(cgi_error.message(), Some("Message")); + assert_eq!(cgi_error.to_code(), CGI_ERROR.0); + + let cgi_error = Temporary::from_str("42\r\n").unwrap(); + assert_eq!(cgi_error.message(), None); + assert_eq!(cgi_error.to_code(), CGI_ERROR.0); + + // 43 + let proxy_error = Temporary::from_str("43 Message\r\n").unwrap(); + assert_eq!(proxy_error.message(), Some("Message")); + assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); + + let proxy_error = Temporary::from_str("43\r\n").unwrap(); + assert_eq!(proxy_error.message(), None); + assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); + + // 44 + let slow_down = Temporary::from_str("44 Message\r\n").unwrap(); + assert_eq!(slow_down.message(), Some("Message")); + assert_eq!(slow_down.to_code(), SLOW_DOWN.0); + + let slow_down = Temporary::from_str("44\r\n").unwrap(); + assert_eq!(slow_down.message(), None); + assert_eq!(slow_down.to_code(), SLOW_DOWN.0); +} diff --git a/src/client/connection/response/failure/temporary/error.rs b/src/client/connection/response/failure/temporary/error.rs new file mode 100644 index 0000000..5cf1cf6 --- /dev/null +++ b/src/client/connection/response/failure/temporary/error.rs @@ -0,0 +1,23 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Code, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Status code error") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + } + } +} diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs new file mode 100644 index 0000000..56dcc08 --- /dev/null +++ b/src/client/connection/response/input.rs @@ -0,0 +1,109 @@ +pub mod error; +pub use error::Error; + +const DEFAULT: (u8, &str) = (10, "Input"); +const SENSITIVE: (u8, &str) = (11, "Sensitive input"); + +pub enum Input { + Default { message: Option }, + Sensitive { message: Option }, +} + +impl Input { + // 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)), + } + } + + // Getters + + pub fn to_code(&self) -> u8 { + match self { + Self::Default { .. } => DEFAULT, + Self::Sensitive { .. } => SENSITIVE, + } + .0 + } + + pub fn message(&self) -> Option<&str> { + match self { + Self::Default { message } => message, + Self::Sensitive { message } => message, + } + .as_deref() + } +} + +impl std::fmt::Display for Input { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), + Self::Sensitive { message } => message.as_deref().unwrap_or(SENSITIVE.1), + } + ) + } +} + +impl std::str::FromStr for Input { + type Err = Error; + fn from_str(header: &str) -> Result { + if let Some(postfix) = header.strip_prefix("10") { + return Ok(Self::Default { + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("11") { + return Ok(Self::Sensitive { + message: message(postfix), + }); + } + Err(Error::Protocol) + } +} + +// Tools + +fn message(value: &str) -> Option { + let value = value.trim(); + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + // 10 + let default = Input::from_str("10 Default\r\n").unwrap(); + assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.message(), Some("Default")); + assert_eq!(default.to_string(), "Default"); + + let default = Input::from_str("10\r\n").unwrap(); + assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.message(), None); + assert_eq!(default.to_string(), DEFAULT.1); + + // 11 + let sensitive = Input::from_str("11 Sensitive\r\n").unwrap(); + assert_eq!(sensitive.to_code(), SENSITIVE.0); + assert_eq!(sensitive.message(), Some("Sensitive")); + assert_eq!(sensitive.to_string(), "Sensitive"); + + let sensitive = Input::from_str("11\r\n").unwrap(); + assert_eq!(sensitive.to_code(), SENSITIVE.0); + assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.to_string(), SENSITIVE.1); +} diff --git a/src/client/connection/response/meta/data/error.rs b/src/client/connection/response/input/error.rs similarity index 59% rename from src/client/connection/response/meta/data/error.rs rename to src/client/connection/response/input/error.rs index 49455cd..ae589e8 100644 --- a/src/client/connection/response/meta/data/error.rs +++ b/src/client/connection/response/input/error.rs @@ -1,16 +1,19 @@ -use std::fmt::{Display, Formatter, Result}; +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; #[derive(Debug)] pub enum Error { - Decode(std::string::FromUtf8Error), Protocol, + Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Decode(e) => { - write!(f, "Decode error: {e}") + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") } Self::Protocol => { write!(f, "Protocol error") diff --git a/src/client/connection/response/meta.rs b/src/client/connection/response/meta.rs deleted file mode 100644 index 26388ad..0000000 --- a/src/client/connection/response/meta.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Components for reading and parsing meta bytes from response: -//! * [Gemini status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) -//! * meta data (for interactive statuses like 10, 11, 30 etc) -//! * MIME type - -pub mod data; -pub mod error; -pub mod mime; -pub mod status; - -pub use data::Data; -pub use error::Error; -pub use mime::Mime; -pub use status::Status; - -use gio::{ - prelude::{IOStreamExt, InputStreamExtManual}, - Cancellable, IOStream, -}; -use glib::{object::IsA, Priority}; - -pub const MAX_LEN: usize = 0x400; // 1024 - -pub struct Meta { - pub status: Status, - pub data: Option, - pub mime: Option, - // @TODO - // charset: Option, - // language: Option, -} - -impl Meta { - // Constructors - - /// Create new `Self` from UTF-8 buffer - /// * supports entire response or just meta slice - pub fn from_utf8(buffer: &[u8]) -> Result { - // Calculate buffer length once - let len = buffer.len(); - - // Parse meta bytes only - match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { - Some(slice) => { - // Parse data - let data = Data::from_utf8(slice); - - if let Err(e) = data { - return Err(Error::Data(e)); - } - - // MIME - - let mime = Mime::from_utf8(slice); - - if let Err(e) = mime { - return Err(Error::Mime(e)); - } - - // Status - - let status = Status::from_utf8(slice); - - if let Err(e) = status { - return Err(Error::Status(e)); - } - - Ok(Self { - data: data.unwrap(), - mime: mime.unwrap(), - status: status.unwrap(), - }) - } - None => Err(Error::Protocol), - } - } - - /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) - pub fn from_stream_async( - stream: impl IsA, - priority: Priority, - cancellable: Cancellable, - on_complete: impl FnOnce(Result) + 'static, - ) { - read_from_stream_async( - Vec::with_capacity(MAX_LEN), - stream, - cancellable, - priority, - |result| match result { - Ok(buffer) => on_complete(Self::from_utf8(&buffer)), - Err(e) => on_complete(Err(e)), - }, - ); - } -} - -// Tools - -/// Asynchronously read all meta bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) -/// -/// Return UTF-8 buffer collected -/// * require `IOStream` reference to keep `Connection` active in async thread -pub fn read_from_stream_async( - mut buffer: Vec, - stream: impl IsA, - cancellable: Cancellable, - priority: Priority, - on_complete: impl FnOnce(Result, Error>) + 'static, -) { - 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() >= MAX_LEN { - return on_complete(Err(Error::Protocol)); - } - - // Read next byte without record - if bytes.contains(&b'\r') { - return read_from_stream_async( - buffer, - stream, - cancellable, - priority, - on_complete, - ); - } - - // Complete without record - if bytes.contains(&b'\n') { - return on_complete(Ok(buffer)); - } - - // Record - buffer.append(&mut bytes); - - // Continue - read_from_stream_async(buffer, stream, cancellable, priority, on_complete); - } - Err((data, e)) => on_complete(Err(Error::InputStream(data, e))), - }, - ); -} diff --git a/src/client/connection/response/meta/charset.rs b/src/client/connection/response/meta/charset.rs deleted file mode 100644 index 1673a59..0000000 --- a/src/client/connection/response/meta/charset.rs +++ /dev/null @@ -1 +0,0 @@ -// @TODO diff --git a/src/client/connection/response/meta/data.rs b/src/client/connection/response/meta/data.rs deleted file mode 100644 index d5eef6b..0000000 --- a/src/client/connection/response/meta/data.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Components for reading and parsing meta **data** bytes from response -//! (e.g. placeholder text for 10, 11, url string for 30, 31 etc) - -pub mod error; -pub use error::Error; - -use glib::GString; - -/// Meta **data** holder -/// -/// For example, `value` could contain: -/// * placeholder text for 10, 11 status -/// * URL string for 30, 31 status -pub struct Data(GString); - -impl Data { - // Constructors - - /// Parse meta **data** from UTF-8 buffer - /// from entire response or just header slice - /// - /// * result could be `None` for some [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - /// that does not expect any data in header - pub fn from_utf8(buffer: &[u8]) -> Result, Error> { - // Define max buffer length for this method - const MAX_LEN: usize = 0x400; // 1024 - - // Init bytes buffer - let mut bytes: Vec = Vec::with_capacity(MAX_LEN); - - // Calculate len once - let len = buffer.len(); - - // Skip 3 bytes for status code of `MAX_LEN` expected - match buffer.get(3..if len > MAX_LEN { MAX_LEN - 3 } else { len }) { - Some(slice) => { - for &byte in slice { - // End of header - if byte == b'\r' { - break; - } - - // Continue - bytes.push(byte); - } - - // Assumes the bytes are valid UTF-8 - match GString::from_utf8(bytes) { - Ok(data) => Ok(match data.is_empty() { - false => Some(Self(data)), - true => None, - }), - Err(e) => Err(Error::Decode(e)), - } - } - None => Err(Error::Protocol), - } - } - - // Getters - - /// Get `Self` as `std::str` - pub fn as_str(&self) -> &str { - self.0.as_str() - } - - /// Get `Self` as `glib::GString` - pub fn as_gstring(&self) -> &GString { - &self.0 - } - - /// Get `glib::GString` copy of `Self` - pub fn to_gstring(&self) -> GString { - self.0.clone() - } -} - -impl std::fmt::Display for Data { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} diff --git a/src/client/connection/response/meta/error.rs b/src/client/connection/response/meta/error.rs deleted file mode 100644 index 55abb24..0000000 --- a/src/client/connection/response/meta/error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - Data(super::data::Error), - InputStream(Vec, glib::Error), - Mime(super::mime::Error), - Protocol, - Status(super::status::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::Data(e) => { - write!(f, "Data error: {e}") - } - Self::InputStream(_, e) => { - // @TODO - write!(f, "Input stream error: {e}") - } - Self::Mime(e) => { - write!(f, "MIME error: {e}") - } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Status(e) => { - write!(f, "Status error: {e}") - } - } - } -} diff --git a/src/client/connection/response/meta/language.rs b/src/client/connection/response/meta/language.rs deleted file mode 100644 index 1673a59..0000000 --- a/src/client/connection/response/meta/language.rs +++ /dev/null @@ -1 +0,0 @@ -// @TODO diff --git a/src/client/connection/response/meta/mime.rs b/src/client/connection/response/meta/mime.rs deleted file mode 100644 index 12b6810..0000000 --- a/src/client/connection/response/meta/mime.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! MIME type parser for different data types - -pub mod error; -pub use error::Error; - -use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; - -/// MIME type holder for `Response` (by [Gemtext specification](https://geminiprotocol.net/docs/gemtext-specification.gmi#media-type-parameters)) -/// * the value stored in lowercase -pub struct Mime(String); - -impl Mime { - // Constructors - - /// Create new `Self` from UTF-8 buffer (that includes **header**) - /// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - pub fn from_utf8(buffer: &[u8]) -> Result, Error> { - // Define max buffer length for this method - const MAX_LEN: usize = 0x400; // 1024 - - // Calculate buffer length once - let len = buffer.len(); - - // Parse meta bytes only - match buffer.get(..if len > MAX_LEN { MAX_LEN } else { len }) { - Some(b) => match std::str::from_utf8(b) { - Ok(s) => Self::from_string(s), - Err(e) => Err(Error::Decode(e)), - }, - None => Err(Error::Protocol), - } - } - - /// Create new `Self` from `str::str` that includes **header** - /// * return `None` for non 2* [status codes](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - pub fn from_string(s: &str) -> Result, Error> { - if !s.starts_with("2") { - return Ok(None); - } - match parse(s) { - Some(v) => Ok(Some(Self(v))), - None => Err(Error::Undefined), - } - } - - // Getters - - /// Get `Self` as lowercase `std::str` - pub fn as_str(&self) -> &str { - self.0.as_str() - } -} - -impl std::fmt::Display for Mime { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Extract MIME type from from string that includes **header** -pub fn parse(s: &str) -> Option { - Regex::split_simple( - r"^2\d{1}\s([^\/]+\/[^\s;]+)", - s, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ) - .get(1) - .map(|this| this.to_lowercase()) -} diff --git a/src/client/connection/response/meta/mime/error.rs b/src/client/connection/response/meta/mime/error.rs deleted file mode 100644 index 5b68dbc..0000000 --- a/src/client/connection/response/meta/mime/error.rs +++ /dev/null @@ -1,22 +0,0 @@ -#[derive(Debug)] -pub enum Error { - Decode(std::str::Utf8Error), - Protocol, - Undefined, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Decode(e) => { - write!(f, "Decode error: {e}") - } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Undefined => { - write!(f, "MIME type undefined") - } - } - } -} diff --git a/src/client/connection/response/meta/status.rs b/src/client/connection/response/meta/status.rs deleted file mode 100644 index 0cd6861..0000000 --- a/src/client/connection/response/meta/status.rs +++ /dev/null @@ -1,109 +0,0 @@ -//! Parser and holder tools for -//! [Status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) - -pub mod error; -pub use error::Error; - -/// Holder for [status code](https://geminiprotocol.net/docs/protocol-specification.gmi#status-codes) -#[derive(Debug)] -pub enum Status { - // Input - Input = 10, - SensitiveInput = 11, - // Success - Success = 20, - // Redirect - Redirect = 30, - PermanentRedirect = 31, - // Temporary failure - TemporaryFailure = 40, - ServerUnavailable = 41, - CgiError = 42, - ProxyError = 43, - SlowDown = 44, - // Permanent failure - PermanentFailure = 50, - NotFound = 51, - ResourceGone = 52, - ProxyRequestRefused = 53, - BadRequest = 59, - // Client certificates - CertificateRequest = 60, - CertificateUnauthorized = 61, - CertificateInvalid = 62, -} - -impl std::fmt::Display for Status { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Status::Input => "Input", - Status::SensitiveInput => "Sensitive Input", - Status::Success => "Success", - Status::Redirect => "Redirect", - Status::PermanentRedirect => "Permanent Redirect", - Status::TemporaryFailure => "Temporary Failure", - Status::ServerUnavailable => "Server Unavailable", - Status::CgiError => "CGI Error", - Status::ProxyError => "Proxy Error", - Status::SlowDown => "Slow Down", - Status::PermanentFailure => "Permanent Failure", - Status::NotFound => "Not Found", - Status::ResourceGone => "Resource Gone", - Status::ProxyRequestRefused => "Proxy Request Refused", - Status::BadRequest => "Bad Request", - Status::CertificateRequest => "Certificate Request", - Status::CertificateUnauthorized => "Certificate Unauthorized", - Status::CertificateInvalid => "Certificate Invalid", - } - ) - } -} - -impl Status { - /// Create new `Self` from UTF-8 buffer - /// - /// * includes `Self::from_string` parser, it means that given buffer should contain some **header** - pub fn from_utf8(buffer: &[u8]) -> Result { - match buffer.get(0..2) { - Some(b) => match std::str::from_utf8(b) { - Ok(s) => Self::from_string(s), - Err(e) => Err(Error::Decode(e)), - }, - None => Err(Error::Protocol), - } - } - - /// Create new `Self` from string that includes **header** - pub fn from_string(code: &str) -> Result { - match code { - // Input - "10" => Ok(Self::Input), - "11" => Ok(Self::SensitiveInput), - // Success - "20" => Ok(Self::Success), - // Redirect - "30" => Ok(Self::Redirect), - "31" => Ok(Self::PermanentRedirect), - // Temporary failure - "40" => Ok(Self::TemporaryFailure), - "41" => Ok(Self::ServerUnavailable), - "42" => Ok(Self::CgiError), - "43" => Ok(Self::ProxyError), - "44" => Ok(Self::SlowDown), - // Permanent failure - "50" => Ok(Self::PermanentFailure), - "51" => Ok(Self::NotFound), - "52" => Ok(Self::ResourceGone), - "53" => Ok(Self::ProxyRequestRefused), - "59" => Ok(Self::BadRequest), - // Client certificates - "60" => Ok(Self::CertificateRequest), - "61" => Ok(Self::CertificateUnauthorized), - "62" => Ok(Self::CertificateInvalid), - _ => Err(Error::Undefined), - } - } -} diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs new file mode 100644 index 0000000..cfb8c21 --- /dev/null +++ b/src/client/connection/response/redirect.rs @@ -0,0 +1,112 @@ +pub mod error; +pub use error::Error; + +use glib::GStringPtr; + +const TEMPORARY: u8 = 30; +const PERMANENT: u8 = 31; + +pub enum Redirect { + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection + Temporary { target: String }, + /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection + Permanent { target: String }, +} + +impl Redirect { + // 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)), + } + } + + // Convertors + + pub fn to_code(&self) -> u8 { + match self { + Self::Permanent { .. } => PERMANENT, + Self::Temporary { .. } => TEMPORARY, + } + } + + // Getters + + pub fn target(&self) -> &str { + match self { + Self::Permanent { target } => target, + Self::Temporary { target } => target, + } + } +} + +impl std::fmt::Display for Redirect { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Permanent { target } => format!("Permanent redirection to `{target}`"), + Self::Temporary { target } => format!("Temporary redirection to `{target}`"), + } + ) + } +} + +impl std::str::FromStr for Redirect { + type Err = Error; + fn from_str(header: &str) -> Result { + use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; + + let regex = Regex::split_simple( + r"^3(\d)\s([^\r\n]+)", + header, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ); + + match regex.get(1) { + Some(code) => match code.as_str() { + "0" => Ok(Self::Temporary { + target: target(regex.get(2))?, + }), + "1" => Ok(Self::Permanent { + target: target(regex.get(2))?, + }), + _ => todo!(), + }, + None => Err(Error::Protocol), + } + } +} + +fn target(value: Option<&GStringPtr>) -> Result { + match value { + Some(target) => { + let target = target.trim(); + if target.is_empty() { + Err(Error::Target) + } else { + Ok(target.to_string()) + } + } + None => Err(Error::Target), + } +} + +#[test] +fn test_from_str() { + use std::str::FromStr; + + let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); + assert_eq!(temporary.target(), "/uri"); + assert_eq!(temporary.to_code(), TEMPORARY); + + let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); + assert_eq!(permanent.target(), "/uri"); + assert_eq!(permanent.to_code(), PERMANENT); +} diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs new file mode 100644 index 0000000..b3d3579 --- /dev/null +++ b/src/client/connection/response/redirect/error.rs @@ -0,0 +1,27 @@ +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; + +#[derive(Debug)] +pub enum Error { + Protocol, + Target, + Utf8Error(Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") + } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Target => { + write!(f, "Target error") + } + } + } +} diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs new file mode 100644 index 0000000..e5ad6f4 --- /dev/null +++ b/src/client/connection/response/success.rs @@ -0,0 +1,89 @@ +pub mod error; +pub use error::Error; + +const DEFAULT: (u8, &str) = (20, "Success"); + +pub enum Success { + Default { mime: String }, + // 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)), + } + } + + // 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), + } + } +} + +#[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); +} diff --git a/src/client/connection/response/meta/status/error.rs b/src/client/connection/response/success/error.rs similarity index 52% rename from src/client/connection/response/meta/status/error.rs rename to src/client/connection/response/success/error.rs index 24090dd..2dbe363 100644 --- a/src/client/connection/response/meta/status/error.rs +++ b/src/client/connection/response/success/error.rs @@ -1,23 +1,26 @@ -use std::fmt::{Display, Formatter, Result}; +use std::{ + fmt::{Display, Formatter, Result}, + str::Utf8Error, +}; #[derive(Debug)] pub enum Error { - Decode(std::str::Utf8Error), Protocol, - Undefined, + Mime, + Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Decode(e) => { - write!(f, "Decode error: {e}") + Self::Utf8Error(e) => { + write!(f, "UTF-8 error: {e}") } Self::Protocol => { write!(f, "Protocol error") } - Self::Undefined => { - write!(f, "Undefined") + Self::Mime => { + write!(f, "MIME error") } } } From 8df7af44b59e2f2cb44e315bc73ed19ae1cbb7d9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 2 Feb 2025 23:08:42 +0200 Subject: [PATCH 04/95] exclude message from string trait --- src/client/connection/response/certificate.rs | 8 ++++--- .../connection/response/failure/permanent.rs | 22 ++++++++++++++----- .../connection/response/failure/temporary.rs | 22 ++++++++++++++----- src/client/connection/response/input.rs | 17 +++++++------- src/client/connection/response/redirect.rs | 16 +++++++++----- 5 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 450d6d7..160e2f0 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -55,10 +55,11 @@ impl std::fmt::Display for Certificate { f, "{}", match self { - Self::Required { message } => message.as_deref().unwrap_or(REQUIRED.1), - Self::NotAuthorized { message } => message.as_deref().unwrap_or(NOT_AUTHORIZED.1), - Self::NotValid { message } => message.as_deref().unwrap_or(NOT_VALID.1), + Self::Required { .. } => REQUIRED, + Self::NotAuthorized { .. } => NOT_AUTHORIZED, + Self::NotValid { .. } => NOT_VALID, } + .1 ) } } @@ -104,6 +105,7 @@ fn test_from_str() { assert_eq!(required.message(), Some("Message")); assert_eq!(required.to_code(), REQUIRED.0); + assert_eq!(required.to_string(), REQUIRED.1); let required = Certificate::from_str("60\r\n").unwrap(); diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index 08b6c56..e2ab9e0 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -64,13 +64,13 @@ impl std::fmt::Display for Permanent { f, "{}", match self { - Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), - Self::NotFound { message } => message.as_deref().unwrap_or(NOT_FOUND.1), - Self::Gone { message } => message.as_deref().unwrap_or(GONE.1), - Self::ProxyRequestRefused { message } => - message.as_deref().unwrap_or(PROXY_REQUEST_REFUSED.1), - Self::BadRequest { message } => message.as_deref().unwrap_or(BAD_REQUEST.1), + Self::Default { .. } => DEFAULT, + Self::NotFound { .. } => NOT_FOUND, + Self::Gone { .. } => GONE, + Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, + Self::BadRequest { .. } => BAD_REQUEST, } + .1 ) } } @@ -126,44 +126,54 @@ fn test_from_str() { let default = Permanent::from_str("50 Message\r\n").unwrap(); assert_eq!(default.message(), Some("Message")); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); let default = Permanent::from_str("50\r\n").unwrap(); assert_eq!(default.message(), None); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); // 51 let not_found = Permanent::from_str("51 Message\r\n").unwrap(); assert_eq!(not_found.message(), Some("Message")); assert_eq!(not_found.to_code(), NOT_FOUND.0); + assert_eq!(not_found.to_string(), NOT_FOUND.1); let not_found = Permanent::from_str("51\r\n").unwrap(); assert_eq!(not_found.message(), None); assert_eq!(not_found.to_code(), NOT_FOUND.0); + assert_eq!(not_found.to_string(), NOT_FOUND.1); // 52 let gone = Permanent::from_str("52 Message\r\n").unwrap(); assert_eq!(gone.message(), Some("Message")); assert_eq!(gone.to_code(), GONE.0); + assert_eq!(gone.to_string(), GONE.1); let gone = Permanent::from_str("52\r\n").unwrap(); assert_eq!(gone.message(), None); assert_eq!(gone.to_code(), GONE.0); + assert_eq!(gone.to_string(), GONE.1); // 53 let proxy_request_refused = Permanent::from_str("53 Message\r\n").unwrap(); assert_eq!(proxy_request_refused.message(), Some("Message")); assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); + assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); let proxy_request_refused = Permanent::from_str("53\r\n").unwrap(); assert_eq!(proxy_request_refused.message(), None); assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); + assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); // 59 let bad_request = Permanent::from_str("59 Message\r\n").unwrap(); assert_eq!(bad_request.message(), Some("Message")); assert_eq!(bad_request.to_code(), BAD_REQUEST.0); + assert_eq!(bad_request.to_string(), BAD_REQUEST.1); let bad_request = Permanent::from_str("59\r\n").unwrap(); assert_eq!(bad_request.message(), None); assert_eq!(bad_request.to_code(), BAD_REQUEST.0); + assert_eq!(bad_request.to_string(), BAD_REQUEST.1); } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 363645e..768bdcd 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -64,13 +64,13 @@ impl std::fmt::Display for Temporary { f, "{}", match self { - Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), - Self::ServerUnavailable { message } => - message.as_deref().unwrap_or(SERVER_UNAVAILABLE.1), - Self::CgiError { message } => message.as_deref().unwrap_or(CGI_ERROR.1), - Self::ProxyError { message } => message.as_deref().unwrap_or(PROXY_ERROR.1), - Self::SlowDown { message } => message.as_deref().unwrap_or(SLOW_DOWN.1), + Self::Default { .. } => DEFAULT, + Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, + Self::CgiError { .. } => CGI_ERROR, + Self::ProxyError { .. } => PROXY_ERROR, + Self::SlowDown { .. } => SLOW_DOWN, } + .1 ) } } @@ -126,44 +126,54 @@ fn test_from_str() { let default = Temporary::from_str("40 Message\r\n").unwrap(); assert_eq!(default.message(), Some("Message")); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); let default = Temporary::from_str("40\r\n").unwrap(); assert_eq!(default.message(), None); assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); // 41 let server_unavailable = Temporary::from_str("41 Message\r\n").unwrap(); assert_eq!(server_unavailable.message(), Some("Message")); assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); + assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); let server_unavailable = Temporary::from_str("41\r\n").unwrap(); assert_eq!(server_unavailable.message(), None); assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); + assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); // 42 let cgi_error = Temporary::from_str("42 Message\r\n").unwrap(); assert_eq!(cgi_error.message(), Some("Message")); assert_eq!(cgi_error.to_code(), CGI_ERROR.0); + assert_eq!(cgi_error.to_string(), CGI_ERROR.1); let cgi_error = Temporary::from_str("42\r\n").unwrap(); assert_eq!(cgi_error.message(), None); assert_eq!(cgi_error.to_code(), CGI_ERROR.0); + assert_eq!(cgi_error.to_string(), CGI_ERROR.1); // 43 let proxy_error = Temporary::from_str("43 Message\r\n").unwrap(); assert_eq!(proxy_error.message(), Some("Message")); assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); + assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); let proxy_error = Temporary::from_str("43\r\n").unwrap(); assert_eq!(proxy_error.message(), None); assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); + assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); // 44 let slow_down = Temporary::from_str("44 Message\r\n").unwrap(); assert_eq!(slow_down.message(), Some("Message")); assert_eq!(slow_down.to_code(), SLOW_DOWN.0); + assert_eq!(slow_down.to_string(), SLOW_DOWN.1); let slow_down = Temporary::from_str("44\r\n").unwrap(); assert_eq!(slow_down.message(), None); assert_eq!(slow_down.to_code(), SLOW_DOWN.0); + assert_eq!(slow_down.to_string(), SLOW_DOWN.1); } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 56dcc08..b62276b 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -46,9 +46,10 @@ impl std::fmt::Display for Input { f, "{}", match self { - Self::Default { message } => message.as_deref().unwrap_or(DEFAULT.1), - Self::Sensitive { message } => message.as_deref().unwrap_or(SENSITIVE.1), + Self::Default { .. } => DEFAULT, + Self::Sensitive { .. } => SENSITIVE, } + .1 ) } } @@ -87,23 +88,23 @@ fn test_from_str() { // 10 let default = Input::from_str("10 Default\r\n").unwrap(); - assert_eq!(default.to_code(), DEFAULT.0); assert_eq!(default.message(), Some("Default")); - assert_eq!(default.to_string(), "Default"); + assert_eq!(default.to_code(), DEFAULT.0); + assert_eq!(default.to_string(), DEFAULT.1); let default = Input::from_str("10\r\n").unwrap(); - assert_eq!(default.to_code(), DEFAULT.0); assert_eq!(default.message(), None); + assert_eq!(default.to_code(), DEFAULT.0); assert_eq!(default.to_string(), DEFAULT.1); // 11 let sensitive = Input::from_str("11 Sensitive\r\n").unwrap(); - assert_eq!(sensitive.to_code(), SENSITIVE.0); assert_eq!(sensitive.message(), Some("Sensitive")); - assert_eq!(sensitive.to_string(), "Sensitive"); + assert_eq!(sensitive.to_code(), SENSITIVE.0); + assert_eq!(sensitive.to_string(), SENSITIVE.1); let sensitive = Input::from_str("11\r\n").unwrap(); - assert_eq!(sensitive.to_code(), SENSITIVE.0); assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.to_code(), SENSITIVE.0); assert_eq!(sensitive.to_string(), SENSITIVE.1); } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index cfb8c21..7c694fd 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -3,8 +3,8 @@ pub use error::Error; use glib::GStringPtr; -const TEMPORARY: u8 = 30; -const PERMANENT: u8 = 31; +const TEMPORARY: (u8, &str) = (30, "Temporary redirect"); +const PERMANENT: (u8, &str) = (31, "Permanent redirect"); pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection @@ -32,6 +32,7 @@ impl Redirect { Self::Permanent { .. } => PERMANENT, Self::Temporary { .. } => TEMPORARY, } + .0 } // Getters @@ -50,9 +51,10 @@ impl std::fmt::Display for Redirect { f, "{}", match self { - Self::Permanent { target } => format!("Permanent redirection to `{target}`"), - Self::Temporary { target } => format!("Temporary redirection to `{target}`"), + Self::Permanent { .. } => PERMANENT, + Self::Temporary { .. } => TEMPORARY, } + .1 ) } } @@ -104,9 +106,11 @@ fn test_from_str() { let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); assert_eq!(temporary.target(), "/uri"); - assert_eq!(temporary.to_code(), TEMPORARY); + assert_eq!(temporary.to_code(), TEMPORARY.0); + assert_eq!(temporary.to_string(), TEMPORARY.1); let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); assert_eq!(permanent.target(), "/uri"); - assert_eq!(permanent.to_code(), PERMANENT); + assert_eq!(permanent.to_code(), PERMANENT.0); + assert_eq!(permanent.to_string(), PERMANENT.1); } From 788b7921674fc10cb07a8ca076c9b0257be98674 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 01:08:31 +0200 Subject: [PATCH 05/95] return connection in result --- src/client.rs | 2 +- src/client/connection.rs | 19 ++++++----- src/client/connection/response.rs | 57 ++++++++++++++++--------------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/client.rs b/src/client.rs index bcce693..f222393 100644 --- a/src/client.rs +++ b/src/client.rs @@ -60,7 +60,7 @@ impl Client { priority: Priority, cancellable: Cancellable, certificate: Option, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static, ) { // Begin new connection // * [NetworkAddress](https://docs.gtk.org/gio/class.NetworkAddress.html) required for valid diff --git a/src/client/connection.rs b/src/client/connection.rs index 9af51ed..b2cee84 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -58,7 +58,7 @@ impl Connection { request: Request, priority: Priority, cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Result<(Response, Self), Error>) + 'static, ) { let output_stream = self.stream().output_stream(); output_stream.clone().write_async( @@ -67,14 +67,17 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match request { - Request::Gemini { .. } => { - Response::from_connection_async(self, priority, cancellable, |result| { + Request::Gemini { .. } => Response::from_connection_async( + self, + priority, + cancellable, + |result, connection| { callback(match result { - Ok(response) => Ok(response), + Ok(response) => Ok((response, connection)), Err(e) => Err(Error::Response(e)), }) - }) - } + }, + ), Request::Titan { data, .. } => output_stream.write_bytes_async( &data, priority, @@ -84,9 +87,9 @@ impl Connection { self, priority, cancellable, - |result| { + |result, connection| { callback(match result { - Ok(response) => Ok(response), + Ok(response) => Ok((response, connection)), Err(e) => Err(Error::Response(e)), }) }, diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 9f2b901..adaf209 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -36,7 +36,7 @@ impl Response { connection: Connection, priority: Priority, cancellable: Cancellable, - callback: impl FnOnce(Result) + 'static, + callback: impl FnOnce(Result, Connection) + 'static, ) { from_stream_async( Vec::with_capacity(HEADER_LEN), @@ -44,35 +44,38 @@ impl Response { cancellable, priority, |result| { - callback(match result { - Ok(buffer) => match buffer.first() { - Some(byte) => match byte { - 1 => match Input::from_utf8(&buffer) { - Ok(input) => Ok(Self::Input(input)), - Err(e) => Err(Error::Input(e)), + callback( + match result { + Ok(buffer) => match buffer.first() { + Some(byte) => match byte { + 1 => match Input::from_utf8(&buffer) { + Ok(input) => Ok(Self::Input(input)), + Err(e) => Err(Error::Input(e)), + }, + 2 => match Success::from_utf8(&buffer) { + Ok(success) => Ok(Self::Success(success)), + Err(e) => Err(Error::Success(e)), + }, + 3 => match Redirect::from_utf8(&buffer) { + Ok(redirect) => Ok(Self::Redirect(redirect)), + Err(e) => Err(Error::Redirect(e)), + }, + 4 | 5 => match Failure::from_utf8(&buffer) { + Ok(failure) => Ok(Self::Failure(failure)), + Err(e) => Err(Error::Failure(e)), + }, + 6 => match Certificate::from_utf8(&buffer) { + Ok(certificate) => Ok(Self::Certificate(certificate)), + Err(e) => Err(Error::Certificate(e)), + }, + b => Err(Error::Code(*b)), }, - 2 => match Success::from_utf8(&buffer) { - Ok(success) => Ok(Self::Success(success)), - Err(e) => Err(Error::Success(e)), - }, - 3 => match Redirect::from_utf8(&buffer) { - Ok(redirect) => Ok(Self::Redirect(redirect)), - Err(e) => Err(Error::Redirect(e)), - }, - 4 | 5 => match Failure::from_utf8(&buffer) { - Ok(failure) => Ok(Self::Failure(failure)), - Err(e) => Err(Error::Failure(e)), - }, - 6 => match Certificate::from_utf8(&buffer) { - Ok(certificate) => Ok(Self::Certificate(certificate)), - Err(e) => Err(Error::Certificate(e)), - }, - b => Err(Error::Code(*b)), + None => Err(Error::Protocol), }, - None => Err(Error::Protocol), + Err(e) => Err(e), }, - Err(e) => Err(e), - }) + connection, + ) }, ); } From a5fbca2ace7d3556692eba1e34d0d915a5806889 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 01:20:08 +0200 Subject: [PATCH 06/95] fix route by first byte --- src/client/connection/response.rs | 12 ++++++------ src/client/connection/response/error.rs | 6 +++--- src/client/connection/response/failure.rs | 6 +++--- src/client/connection/response/failure/error.rs | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index adaf209..9c66dd3 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -48,27 +48,27 @@ impl Response { match result { Ok(buffer) => match buffer.first() { Some(byte) => match byte { - 1 => match Input::from_utf8(&buffer) { + 0x31 => match Input::from_utf8(&buffer) { Ok(input) => Ok(Self::Input(input)), Err(e) => Err(Error::Input(e)), }, - 2 => match Success::from_utf8(&buffer) { + 0x32 => match Success::from_utf8(&buffer) { Ok(success) => Ok(Self::Success(success)), Err(e) => Err(Error::Success(e)), }, - 3 => match Redirect::from_utf8(&buffer) { + 0x33 => match Redirect::from_utf8(&buffer) { Ok(redirect) => Ok(Self::Redirect(redirect)), Err(e) => Err(Error::Redirect(e)), }, - 4 | 5 => match Failure::from_utf8(&buffer) { + 0x34 | 0x35 => match Failure::from_utf8(&buffer) { Ok(failure) => Ok(Self::Failure(failure)), Err(e) => Err(Error::Failure(e)), }, - 6 => match Certificate::from_utf8(&buffer) { + 0x36 => match Certificate::from_utf8(&buffer) { Ok(certificate) => Ok(Self::Certificate(certificate)), Err(e) => Err(Error::Certificate(e)), }, - b => Err(Error::Code(*b)), + _ => Err(Error::Code), }, None => Err(Error::Protocol), }, diff --git a/src/client/connection/response/error.rs b/src/client/connection/response/error.rs index 99895d9..df8cda4 100644 --- a/src/client/connection/response/error.rs +++ b/src/client/connection/response/error.rs @@ -6,7 +6,7 @@ use std::{ #[derive(Debug)] pub enum Error { Certificate(super::certificate::Error), - Code(u8), + Code, Failure(super::failure::Error), Input(super::input::Error), Protocol, @@ -22,8 +22,8 @@ impl Display for Error { Self::Certificate(e) => { write!(f, "Certificate error: {e}") } - Self::Code(e) => { - write!(f, "Code group error: {e}*") + Self::Code => { + write!(f, "Code group error") } Self::Failure(e) => { write!(f, "Failure error: {e}") diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index c0e76ec..19f6461 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -22,15 +22,15 @@ impl Failure { pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.first() { Some(byte) => match byte { - 4 => match Temporary::from_utf8(buffer) { + 0x34 => match Temporary::from_utf8(buffer) { Ok(input) => Ok(Self::Temporary(input)), Err(e) => Err(Error::Temporary(e)), }, - 5 => match Permanent::from_utf8(buffer) { + 0x35 => match Permanent::from_utf8(buffer) { Ok(failure) => Ok(Self::Permanent(failure)), Err(e) => Err(Error::Permanent(e)), }, - b => Err(Error::Code(*b)), + _ => Err(Error::Code), }, None => Err(Error::Protocol), } diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs index 60724eb..7725b92 100644 --- a/src/client/connection/response/failure/error.rs +++ b/src/client/connection/response/failure/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code(u8), + Code, Permanent(super::permanent::Error), Protocol, Temporary(super::temporary::Error), @@ -11,8 +11,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code(e) => { - write!(f, "Code group error: {e}*") + Self::Code => { + write!(f, "Code group error") } Self::Permanent(e) => { write!(f, "Permanent failure group error: {e}") From dc2300b1c0d8b9cd818224d21e06b6c8ead598f0 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 01:47:50 +0200 Subject: [PATCH 07/95] use human-readable bytes format --- src/client/connection/response.rs | 14 ++++++-------- src/client/connection/response/failure.rs | 4 ++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 9c66dd3..6c1cd7b 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -1,5 +1,3 @@ -//! Read and parse Gemini response as Object - pub mod certificate; pub mod data; // @TODO deprecated pub mod error; @@ -19,7 +17,7 @@ use super::Connection; use gio::{Cancellable, IOStream}; use glib::{object::IsA, Priority}; -const HEADER_LEN: usize = 0x400; // 1024 +const HEADER_LEN: usize = 1024; /// https://geminiprotocol.net/docs/protocol-specification.gmi#responses pub enum Response { @@ -48,23 +46,23 @@ impl Response { match result { Ok(buffer) => match buffer.first() { Some(byte) => match byte { - 0x31 => match Input::from_utf8(&buffer) { + b'1' => match Input::from_utf8(&buffer) { Ok(input) => Ok(Self::Input(input)), Err(e) => Err(Error::Input(e)), }, - 0x32 => match Success::from_utf8(&buffer) { + b'2' => match Success::from_utf8(&buffer) { Ok(success) => Ok(Self::Success(success)), Err(e) => Err(Error::Success(e)), }, - 0x33 => match Redirect::from_utf8(&buffer) { + b'3' => match Redirect::from_utf8(&buffer) { Ok(redirect) => Ok(Self::Redirect(redirect)), Err(e) => Err(Error::Redirect(e)), }, - 0x34 | 0x35 => match Failure::from_utf8(&buffer) { + b'4' | b'5' => match Failure::from_utf8(&buffer) { Ok(failure) => Ok(Self::Failure(failure)), Err(e) => Err(Error::Failure(e)), }, - 0x36 => match Certificate::from_utf8(&buffer) { + b'6' => match Certificate::from_utf8(&buffer) { Ok(certificate) => Ok(Self::Certificate(certificate)), Err(e) => Err(Error::Certificate(e)), }, diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 19f6461..40c8abf 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -22,11 +22,11 @@ impl Failure { pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.first() { Some(byte) => match byte { - 0x34 => match Temporary::from_utf8(buffer) { + b'4' => match Temporary::from_utf8(buffer) { Ok(input) => Ok(Self::Temporary(input)), Err(e) => Err(Error::Temporary(e)), }, - 0x35 => match Permanent::from_utf8(buffer) { + b'5' => match Permanent::from_utf8(buffer) { Ok(failure) => Ok(Self::Permanent(failure)), Err(e) => Err(Error::Permanent(e)), }, From 517153656b50509c5afda6440b6d611771d672b9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 02:09:41 +0200 Subject: [PATCH 08/95] update example --- README.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 23ee9de..57c3f75 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,10 @@ cargo add ggemini use gio::*; use glib::*; + use ggemini::client::{ - connection::{ - Request, Response, - response::meta::{Mime, Status} - }, - Client, Error, + connection::{response::Success, Request, Response}, + Client, }; fn main() -> ExitCode { @@ -58,20 +56,17 @@ fn main() -> ExitCode { Priority::DEFAULT, Cancellable::new(), None, // optional `GTlsCertificate` - |result: Result| match result { - Ok(response) => { - // route by status code - match response.meta.status { - // code 20, handle `GIOStream` by content type - Status::Success => match response.meta.mime.unwrap().as_str() { - // gemtext, see ggemtext crate to parse + |result| match result { + Ok((response, _connection)) => match response { + Response::Success(success) => match success { + Success::Default { mime } => match mime.as_str() { "text/gemini" => todo!(), - // other content types _ => todo!(), }, _ => todo!(), - } - } + }, + _ => todo!(), + }, Err(_) => todo!(), }, ); From 7518101b552fc58a42ce9d7baa68aa5e2ee6229f Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 02:44:33 +0200 Subject: [PATCH 09/95] implement `to_uri` method --- src/client/connection/response/redirect.rs | 62 ++++++++++++++++++- .../connection/response/redirect/error.rs | 4 ++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 7c694fd..b0856be 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -1,7 +1,7 @@ pub mod error; pub use error::Error; -use glib::GStringPtr; +use glib::{GStringPtr, Uri, UriFlags}; const TEMPORARY: (u8, &str) = (30, "Temporary redirect"); const PERMANENT: (u8, &str) = (31, "Permanent redirect"); @@ -35,6 +35,32 @@ impl Redirect { .0 } + /// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), + /// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` + /// * fragment implementation uncompleted @TODO + pub fn to_uri(&self, base: &Uri) -> Result { + match Uri::build( + UriFlags::NONE, + base.scheme().as_str(), + None, // unexpected + base.host().as_deref(), + base.port(), + base.path().as_str(), + // > If a server sends a redirection in response to a request with a query string, + // > the client MUST NOT apply the query string to the new location + None, + // > A server SHOULD NOT include fragments in redirections, + // > but if one is given, and a client already has a fragment it could apply (from the original URI), + // > it is up to the client which fragment to apply. + None, // @TODO + ) + .parse_relative(self.target(), UriFlags::NONE) + { + Ok(absolute) => Ok(absolute), + Err(e) => Err(Error::Glib(e)), + } + } + // Getters pub fn target(&self) -> &str { @@ -114,3 +140,37 @@ fn test_from_str() { assert_eq!(permanent.to_code(), PERMANENT.0); assert_eq!(permanent.to_string(), PERMANENT.1); } + +#[test] +fn test_to_uri() { + use std::str::FromStr; + + let request = Uri::build( + UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("?query"), + Some("?fragment"), + ); + + let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&request).unwrap().to_string(), + "gemini://geminiprotocol.net/uri" + ); + + let resolve = Redirect::from_str("30 uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&request).unwrap().to_string(), + "gemini://geminiprotocol.net/path/uri" + ); + + let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&request).unwrap().to_string(), + "gemini://test.host/uri" + ); +} diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index b3d3579..029abf6 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -5,6 +5,7 @@ use std::{ #[derive(Debug)] pub enum Error { + Glib(glib::Error), Protocol, Target, Utf8Error(Utf8Error), @@ -13,6 +14,9 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Glib(e) => { + write!(f, "Glib error: {e}") + } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } From d57d9fc7df9241f22ebf441473ee7658ea634461 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 02:55:42 +0200 Subject: [PATCH 10/95] update enum name --- src/client/connection/response/redirect.rs | 2 +- src/client/connection/response/redirect/error.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index b0856be..8e1cb24 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -57,7 +57,7 @@ impl Redirect { .parse_relative(self.target(), UriFlags::NONE) { Ok(absolute) => Ok(absolute), - Err(e) => Err(Error::Glib(e)), + Err(e) => Err(Error::Uri(e)), } } diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index 029abf6..acee073 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -5,7 +5,7 @@ use std::{ #[derive(Debug)] pub enum Error { - Glib(glib::Error), + Uri(glib::Error), Protocol, Target, Utf8Error(Utf8Error), @@ -14,8 +14,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Glib(e) => { - write!(f, "Glib error: {e}") + Self::Uri(e) => { + write!(f, "URI error: {e}") } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") From 9bb926f243d51717b230245b4b6c61c43b3afe93 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 03:03:36 +0200 Subject: [PATCH 11/95] update example --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 57c3f75..98fe828 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ use glib::*; use ggemini::client::{ - connection::{response::Success, Request, Response}, + connection::{Request, Response}, Client, }; @@ -59,11 +59,10 @@ fn main() -> ExitCode { |result| match result { Ok((response, _connection)) => match response { Response::Success(success) => match success { - Success::Default { mime } => match mime.as_str() { + _ => match success.mime() { "text/gemini" => todo!(), _ => todo!(), - }, - _ => todo!(), + } }, _ => todo!(), }, From 041454d8df074405cf30291c6880b1d0612255a7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 03:10:25 +0200 Subject: [PATCH 12/95] update example --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 98fe828..90105f5 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ cargo add ggemini use gio::*; use glib::*; - use ggemini::client::{ connection::{Request, Response}, Client, @@ -58,11 +57,9 @@ fn main() -> ExitCode { None, // optional `GTlsCertificate` |result| match result { Ok((response, _connection)) => match response { - Response::Success(success) => match success { - _ => match success.mime() { - "text/gemini" => todo!(), - _ => todo!(), - } + Response::Success(success) => match success.mime() { + "text/gemini" => todo!(), + _ => todo!(), }, _ => todo!(), }, From 998a4e97b46283faa5e39509551d4611e33948bf Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 11:27:10 +0200 Subject: [PATCH 13/95] fix uri arguments --- src/client/connection/response/redirect.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 8e1cb24..add308c 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -152,8 +152,8 @@ fn test_to_uri() { Some("geminiprotocol.net"), -1, "/path/", - Some("?query"), - Some("?fragment"), + Some("query"), + Some("fragment"), ); let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); From c9d5e5987cb4e86dcb2900e027a62aacee6c6c3e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:05:37 +0200 Subject: [PATCH 14/95] minor optimizations --- src/gio/memory_input_stream.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 30f9fd8..f268ec6 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -40,19 +40,12 @@ pub fn move_all_from_stream_async( memory_input_stream: MemoryInputStream, cancellable: Cancellable, priority: Priority, - bytes: ( - usize, // bytes_in_chunk - usize, // bytes_total_limit - usize, // bytes_total - ), - callback: ( - impl Fn(Bytes, usize) + 'static, // on_chunk - impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, // on_complete + (bytes_in_chunk, bytes_total_limit, mut bytes_total): (usize, usize, usize), + (on_chunk, on_complete): ( + impl Fn(Bytes, usize) + 'static, + impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { - let (on_chunk, on_complete) = callback; - let (bytes_in_chunk, bytes_total_limit, bytes_total) = bytes; - base_io_stream.input_stream().read_bytes_async( bytes_in_chunk, priority, @@ -60,7 +53,7 @@ pub fn move_all_from_stream_async( move |result| match result { Ok(bytes) => { // Update bytes total - let bytes_total = bytes_total + bytes.len(); + bytes_total += bytes.len(); // Callback chunk function on_chunk(bytes.clone(), bytes_total); @@ -92,5 +85,5 @@ pub fn move_all_from_stream_async( on_complete(Err(Error::InputStream(e))); } }, - ); + ) } From 1505b6311c7f03756e6880667afb1110f7fc321d Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:38:04 +0200 Subject: [PATCH 15/95] rename arguments, use tuple for arguments group --- src/gio/memory_input_stream.rs | 49 +++++++++++++++------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index f268ec6..8404f7f 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -5,7 +5,7 @@ use gio::{ prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, Cancellable, IOStream, MemoryInputStream, }; -use glib::{object::IsA, Bytes, Priority}; +use glib::{object::IsA, Priority}; /// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) @@ -14,20 +14,21 @@ use glib::{object::IsA, Bytes, Priority}; /// * safe read (of memory overflow) to dynamically allocated buffer, where final size of target data unknown /// * calculate bytes processed on chunk load pub fn from_stream_async( - base_io_stream: impl IsA, + io_stream: impl IsA, cancelable: Cancellable, priority: Priority, - bytes_in_chunk: usize, - bytes_total_limit: usize, - on_chunk: impl Fn(Bytes, usize) + 'static, - on_complete: impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, + (chunk, limit): (usize, usize), + (on_chunk, on_complete): ( + impl Fn(usize, usize) + 'static, + impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, + ), ) { move_all_from_stream_async( - base_io_stream, + io_stream, MemoryInputStream::new(), cancelable, priority, - (bytes_in_chunk, bytes_total_limit, 0), + (chunk, limit, 0), (on_chunk, on_complete), ); } @@ -36,48 +37,42 @@ pub fn from_stream_async( /// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread pub fn move_all_from_stream_async( - base_io_stream: impl IsA, + io_stream: impl IsA, memory_input_stream: MemoryInputStream, cancellable: Cancellable, priority: Priority, - (bytes_in_chunk, bytes_total_limit, mut bytes_total): (usize, usize, usize), + (chunk, limit, mut total): (usize, usize, usize), (on_chunk, on_complete): ( - impl Fn(Bytes, usize) + 'static, + impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { - base_io_stream.input_stream().read_bytes_async( - bytes_in_chunk, + io_stream.input_stream().read_bytes_async( + chunk, priority, Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - // Update bytes total - bytes_total += bytes.len(); + total += bytes.len(); + on_chunk(bytes.len(), total); - // Callback chunk function - on_chunk(bytes.clone(), bytes_total); - - // Validate max size - if bytes_total > bytes_total_limit { - return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); + if total > limit { + return on_complete(Err(Error::BytesTotal(total, limit))); } - // No bytes were read, end of stream if bytes.len() == 0 { - return on_complete(Ok((memory_input_stream, bytes_total))); + return on_complete(Ok((memory_input_stream, total))); } - // Write chunk bytes memory_input_stream.add_bytes(&bytes); - // Continue + // continue reading.. move_all_from_stream_async( - base_io_stream, + io_stream, memory_input_stream, cancellable, priority, - (bytes_in_chunk, bytes_total_limit, bytes_total), + (chunk, limit, total), (on_chunk, on_complete), ); } From 4ee92645ca090d81712239d436831f1baf867187 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:38:34 +0200 Subject: [PATCH 16/95] remove deprecated features --- src/client/connection/response.rs | 1 - src/client/connection/response/data.rs | 7 -- src/client/connection/response/data/text.rs | 113 ------------------ .../connection/response/data/text/error.rs | 24 ---- 4 files changed, 145 deletions(-) delete mode 100644 src/client/connection/response/data.rs delete mode 100644 src/client/connection/response/data/text.rs delete mode 100644 src/client/connection/response/data/text/error.rs diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 6c1cd7b..51c3c7e 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -1,5 +1,4 @@ pub mod certificate; -pub mod data; // @TODO deprecated pub mod error; pub mod failure; pub mod input; diff --git a/src/client/connection/response/data.rs b/src/client/connection/response/data.rs deleted file mode 100644 index 0009fe8..0000000 --- a/src/client/connection/response/data.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Gemini response could have different MIME type for data. -//! Use one of components below to parse response according to content type expected. -//! -//! * MIME type could be detected using `client::response::Meta` parser - -pub mod text; -pub use text::Text; diff --git a/src/client/connection/response/data/text.rs b/src/client/connection/response/data/text.rs deleted file mode 100644 index aaa8aa1..0000000 --- a/src/client/connection/response/data/text.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Tools for Text-based response - -pub mod error; -pub use error::Error; - -// Local dependencies -use gio::{ - prelude::{IOStreamExt, InputStreamExt}, - Cancellable, IOStream, -}; -use glib::{object::IsA, GString, Priority}; - -// Default limits -pub const BUFFER_CAPACITY: usize = 0x400; // 1024 -pub const BUFFER_MAX_SIZE: usize = 0xfffff; // 1M - -/// Container for text-based response data -pub struct Text(String); - -impl Default for Text { - fn default() -> Self { - Self::new() - } -} - -impl Text { - // Constructors - - /// Create new `Self` - pub fn new() -> Self { - Self(String::new()) - } - - /// Create new `Self` from string - pub fn from_string(data: &str) -> Self { - Self(data.into()) - } - - /// Create new `Self` from UTF-8 buffer - pub fn from_utf8(buffer: &[u8]) -> Result { - match GString::from_utf8(buffer.into()) { - Ok(data) => Ok(Self::from_string(&data)), - Err(e) => Err(Error::Decode(e)), - } - } - - /// Asynchronously create new `Self` from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) - pub fn from_stream_async( - stream: impl IsA, - priority: Priority, - cancellable: Cancellable, - on_complete: impl FnOnce(Result) + 'static, - ) { - read_all_from_stream_async( - Vec::with_capacity(BUFFER_CAPACITY), - stream, - cancellable, - priority, - |result| match result { - Ok(buffer) => on_complete(Self::from_utf8(&buffer)), - Err(e) => on_complete(Err(e)), - }, - ); - } -} - -impl std::fmt::Display for Text { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -// Tools - -/// Asynchronously read all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) -/// -/// Return UTF-8 buffer collected -/// * require `IOStream` reference to keep `Connection` active in async thread -pub fn read_all_from_stream_async( - mut buffer: Vec, - stream: impl IsA, - cancelable: Cancellable, - priority: Priority, - callback: impl FnOnce(Result, Error>) + 'static, -) { - stream.input_stream().read_bytes_async( - BUFFER_CAPACITY, - priority, - Some(&cancelable.clone()), - move |result| match result { - Ok(bytes) => { - // No bytes were read, end of stream - if bytes.len() == 0 { - return callback(Ok(buffer)); - } - - // Validate overflow - if buffer.len() + bytes.len() > BUFFER_MAX_SIZE { - return callback(Err(Error::BufferOverflow)); - } - - // Save chunks to buffer - for &byte in bytes.iter() { - buffer.push(byte); - } - - // Continue bytes reading - read_all_from_stream_async(buffer, stream, cancelable, priority, callback); - } - Err(e) => callback(Err(Error::InputStream(e))), - }, - ); -} diff --git a/src/client/connection/response/data/text/error.rs b/src/client/connection/response/data/text/error.rs deleted file mode 100644 index 4a853aa..0000000 --- a/src/client/connection/response/data/text/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::fmt::{Display, Formatter, Result}; - -#[derive(Debug)] -pub enum Error { - BufferOverflow, - Decode(std::string::FromUtf8Error), - InputStream(glib::Error), -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> Result { - match self { - Self::BufferOverflow => { - write!(f, "Buffer overflow") - } - Self::Decode(e) => { - write!(f, "Decode error: {e}") - } - Self::InputStream(e) => { - write!(f, "Input stream read error: {e}") - } - } - } -} From 46483d1829c61260e0591de73d23bf30b1565f67 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:48:57 +0200 Subject: [PATCH 17/95] rename methods, change arguments order --- src/gio/memory_input_stream.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 8404f7f..c953bc5 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -15,19 +15,19 @@ use glib::{object::IsA, Priority}; /// * calculate bytes processed on chunk load pub fn from_stream_async( io_stream: impl IsA, - cancelable: Cancellable, priority: Priority, + cancelable: Cancellable, (chunk, limit): (usize, usize), (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { - move_all_from_stream_async( - io_stream, + for_memory_input_stream_async( MemoryInputStream::new(), - cancelable, + io_stream, priority, + cancelable, (chunk, limit, 0), (on_chunk, on_complete), ); @@ -36,11 +36,11 @@ pub fn from_stream_async( /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread -pub fn move_all_from_stream_async( - io_stream: impl IsA, +pub fn for_memory_input_stream_async( memory_input_stream: MemoryInputStream, - cancellable: Cancellable, + io_stream: impl IsA, priority: Priority, + cancellable: Cancellable, (chunk, limit, mut total): (usize, usize, usize), (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, @@ -67,14 +67,14 @@ pub fn move_all_from_stream_async( memory_input_stream.add_bytes(&bytes); // continue reading.. - move_all_from_stream_async( - io_stream, + for_memory_input_stream_async( memory_input_stream, - cancellable, + io_stream, priority, + cancellable, (chunk, limit, total), (on_chunk, on_complete), - ); + ) } Err(e) => { on_complete(Err(Error::InputStream(e))); From 867945ec74b864eb4c9a706a436e7d0d678b4464 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:55:31 +0200 Subject: [PATCH 18/95] rename method, apply minor optimizations --- src/gio/file_output_stream.rs | 47 ++++++++++++++--------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index b34b507..ce5fac1 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -10,49 +10,40 @@ use glib::{object::IsA, Bytes, Priority}; /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread -pub fn move_all_from_stream_async( - base_io_stream: impl IsA, +pub fn from_stream_async( + io_stream: impl IsA, file_output_stream: FileOutputStream, cancellable: Cancellable, priority: Priority, - bytes: ( + (chunk, limit, mut total): ( usize, // bytes_in_chunk Option, // bytes_total_limit, `None` to unlimited usize, // bytes_total ), - callback: ( + (on_chunk, on_complete): ( impl Fn(Bytes, usize) + 'static, // on_chunk impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete ), ) { - let (on_chunk, on_complete) = callback; - let (bytes_in_chunk, bytes_total_limit, bytes_total) = bytes; - - base_io_stream.input_stream().read_bytes_async( - bytes_in_chunk, + io_stream.input_stream().read_bytes_async( + chunk, priority, Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - // Update bytes total - let bytes_total = bytes_total + bytes.len(); + total += bytes.len(); + on_chunk(bytes.clone(), total); - // Callback chunk function - on_chunk(bytes.clone(), bytes_total); - - // Validate max size - if let Some(bytes_total_limit) = bytes_total_limit { - if bytes_total > bytes_total_limit { - return on_complete(Err(Error::BytesTotal(bytes_total, bytes_total_limit))); + if let Some(limit) = limit { + if total > limit { + return on_complete(Err(Error::BytesTotal(total, limit))); } } - // No bytes were read, end of stream if bytes.len() == 0 { - return on_complete(Ok((file_output_stream, bytes_total))); + return on_complete(Ok((file_output_stream, total))); } - // Write chunk bytes file_output_stream.clone().write_async( bytes.clone(), priority, @@ -60,13 +51,13 @@ pub fn move_all_from_stream_async( move |result| { match result { Ok(_) => { - // Continue - move_all_from_stream_async( - base_io_stream, + // continue read.. + from_stream_async( + io_stream, file_output_stream, cancellable, priority, - (bytes_in_chunk, bytes_total_limit, bytes_total), + (chunk, limit, total), (on_chunk, on_complete), ); } @@ -77,9 +68,7 @@ pub fn move_all_from_stream_async( }, ); } - Err(e) => { - on_complete(Err(Error::InputStream(e))); - } + Err(e) => on_complete(Err(Error::InputStream(e))), }, - ); + ) } From a9536011417d443429565a44995a22aa6c3995d7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 13:59:40 +0200 Subject: [PATCH 19/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f54e4c8..d4c6b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.0" +version = "0.15.1" edition = "2021" license = "MIT" readme = "README.md" From b3e3f2e07b7a2a79f60323c8b300340ab668f254 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 14:02:19 +0200 Subject: [PATCH 20/95] rollback release version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d4c6b32..f54e4c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.1" +version = "0.15.0" edition = "2021" license = "MIT" readme = "README.md" From d4f076f074647c63f0a4fc5647e929d738f0fbc7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 3 Feb 2025 14:04:49 +0200 Subject: [PATCH 21/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f54e4c8..d4c6b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.0" +version = "0.15.1" edition = "2021" license = "MIT" readme = "README.md" From 62f53304aaceb450f48bb29d19e24f647f66a8ad Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 9 Feb 2025 00:42:57 +0200 Subject: [PATCH 22/95] stop chunk iteration on match `len < chunk` condition (some servers may close the connection immediately); hold `memory_input_stream` in the error returned --- Cargo.toml | 2 +- src/gio/memory_input_stream.rs | 30 +++++++++++++++++----------- src/gio/memory_input_stream/error.rs | 8 ++++---- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4c6b32..25b5fde 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.15.1" +version = "0.16.0" edition = "2021" license = "MIT" readme = "README.md" diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index c953bc5..c588f9d 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -53,19 +53,27 @@ pub fn for_memory_input_stream_async( Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - total += bytes.len(); - on_chunk(bytes.len(), total); + let len = bytes.len(); // calculate once - if total > limit { - return on_complete(Err(Error::BytesTotal(total, limit))); - } - - if bytes.len() == 0 { - return on_complete(Ok((memory_input_stream, total))); - } + total += len; + on_chunk(len, total); memory_input_stream.add_bytes(&bytes); + // prevent memory overflow on size `limit` reached + // * add last received bytes into the `memory_input_stream` anyway (to prevent data lost), + // it's safe because limited to the `chunk` size + if total > limit { + return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); + } + + // is the next iteration required? + if len < chunk // some servers may close the connection after first chunk request (@TODO this condition wants review) + || len == 0 + { + return on_complete(Ok((memory_input_stream, total))); + } + // continue reading.. for_memory_input_stream_async( memory_input_stream, @@ -76,9 +84,7 @@ pub fn for_memory_input_stream_async( (on_chunk, on_complete), ) } - Err(e) => { - on_complete(Err(Error::InputStream(e))); - } + Err(e) => on_complete(Err(Error::InputStream(memory_input_stream, e))), }, ) } diff --git a/src/gio/memory_input_stream/error.rs b/src/gio/memory_input_stream/error.rs index 673906c..6b8ae86 100644 --- a/src/gio/memory_input_stream/error.rs +++ b/src/gio/memory_input_stream/error.rs @@ -2,17 +2,17 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - BytesTotal(usize, usize), - InputStream(glib::Error), + BytesTotal(gio::MemoryInputStream, usize, usize), + InputStream(gio::MemoryInputStream, glib::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::BytesTotal(total, limit) => { + Self::BytesTotal(_, total, limit) => { write!(f, "Bytes total limit reached: {total} / {limit}") } - Self::InputStream(e) => { + Self::InputStream(_, e) => { write!(f, "Input stream error: {e}") } } From bb8c2273d4080eaa3cea4f9fd386230adceffbc9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 9 Feb 2025 02:02:55 +0200 Subject: [PATCH 23/95] remove unspecified condition, skip handle the chunk on zero bytes received --- src/gio/memory_input_stream.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index c588f9d..2500087 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -55,9 +55,16 @@ pub fn for_memory_input_stream_async( Ok(bytes) => { let len = bytes.len(); // calculate once + // is the end of stream + if len == 0 { + return on_complete(Ok((memory_input_stream, total))); + } + + // handle the chunk total += len; on_chunk(len, total); + // push bytes into the memory pool memory_input_stream.add_bytes(&bytes); // prevent memory overflow on size `limit` reached @@ -67,13 +74,6 @@ pub fn for_memory_input_stream_async( return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); } - // is the next iteration required? - if len < chunk // some servers may close the connection after first chunk request (@TODO this condition wants review) - || len == 0 - { - return on_complete(Ok((memory_input_stream, total))); - } - // continue reading.. for_memory_input_stream_async( memory_input_stream, From 582744f8304819567a6d110a891aaf42e166d19e Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 9 Feb 2025 02:50:34 +0200 Subject: [PATCH 24/95] update comments --- src/gio/memory_input_stream.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 2500087..def3845 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -55,26 +55,24 @@ pub fn for_memory_input_stream_async( Ok(bytes) => { let len = bytes.len(); // calculate once - // is the end of stream + // is end of stream if len == 0 { return on_complete(Ok((memory_input_stream, total))); } - // handle the chunk + // callback chunk function total += len; on_chunk(len, total); // push bytes into the memory pool memory_input_stream.add_bytes(&bytes); - // prevent memory overflow on size `limit` reached - // * add last received bytes into the `memory_input_stream` anyway (to prevent data lost), - // it's safe because limited to the `chunk` size + // prevent memory overflow if total > limit { return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); } - // continue reading.. + // handle next chunk.. for_memory_input_stream_async( memory_input_stream, io_stream, From 83b29c027660ca402ccaf46a99f1bbf21df0b907 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 00:17:08 +0200 Subject: [PATCH 25/95] use common callback --- src/client.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index f222393..79f51c4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -82,9 +82,11 @@ impl Client { request, priority, cancellable, - move |result| match result { - Ok(response) => callback(Ok(response)), - Err(e) => callback(Err(Error::Connection(e))), + move |result| { + callback(match result { + Ok(response) => Ok(response), + Err(e) => Err(Error::Connection(e)), + }) }, ), Err(e) => callback(Err(Error::Connection(e))), From 0f1caadc03bb24a30de7c819073e232bc895d94e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 01:57:35 +0200 Subject: [PATCH 26/95] update comment --- src/client/connection.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index b2cee84..2ad832e 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -129,7 +129,9 @@ pub fn new_tls_client_connection( // Prevent session resumption (certificate change ability in runtime) tls_client_connection.set_property("session-resumption-enabled", is_session_resumption); - // @TODO handle + // Return `Err` on server connection mismatch following specification lines: + // > Gemini servers MUST use the TLS close_notify implementation to close the connection + // > A client SHOULD notify the user of such a case // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections tls_client_connection.set_require_close_notify(true); From 8334d8a83ca640610412910095f3390f4fa175f7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 02:00:54 +0200 Subject: [PATCH 27/95] update comment --- src/client/connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 2ad832e..ec29195 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -118,7 +118,7 @@ impl Connection { /// 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 [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( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, From 9ce509cedc8f2a92bd5bb9125c4d219f4f38d48b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 10 Feb 2025 02:02:45 +0200 Subject: [PATCH 28/95] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 90105f5..cd21d88 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Glib/Gio-oriented network API for [Gemini protocol](https://geminiprotocol.net/) > Project in development! > -GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) and/or [gio](https://crates.io/crates/gio) (`2.66+`) bindings. +GGemini (or G-Gemini) library written as the client extension for [Yoda](https://github.com/YGGverse/Yoda), it also could be useful for other GTK-based applications dependent of [glib](https://crates.io/crates/glib) / [gio](https://crates.io/crates/gio) (`2.66+`) backend. ## Requirements From 862ab1ccfa6a6ca97507bbf5eda86f6d8446363c Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 13 Feb 2025 07:14:41 +0200 Subject: [PATCH 29/95] increase default timeout to 30 seconds --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 79f51c4..342eac3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,7 @@ use glib::Priority; // Defaults -pub const DEFAULT_TIMEOUT: u32 = 10; +pub const DEFAULT_TIMEOUT: u32 = 30; pub const DEFAULT_SESSION_RESUMPTION: bool = false; /// Main point where connect external crate From e635c410654a96d1373f684de0bdcd902082a3ae Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Feb 2025 21:11:41 +0200 Subject: [PATCH 30/95] add funding info --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ada8a24 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://yggverse.github.io/#donate \ No newline at end of file From f51c636401e7e8f38c8da155d9d2d27372d659fa Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Feb 2025 23:11:17 +0200 Subject: [PATCH 31/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 25b5fde..19c3745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.16.0" +version = "0.16.1" edition = "2021" license = "MIT" readme = "README.md" From 4f6799a495dcfdecfaddfd2afbe4e8ed05370609 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:38:09 +0200 Subject: [PATCH 32/95] remove extra clone --- src/gio/file_output_stream.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index ce5fac1..9a62667 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -61,9 +61,7 @@ pub fn from_stream_async( (on_chunk, on_complete), ); } - Err((bytes, e)) => { - on_complete(Err(Error::OutputStream(bytes.clone(), e))) - } + Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), } }, ); From 1ff38ee83813d28f622b3acfae51a9563bfd2f62 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:49:41 +0200 Subject: [PATCH 33/95] fix maximum payload of 16 kB by using `write_all` method, hold bytes on request error --- src/client/connection.rs | 22 ++++++++++++++-------- src/client/connection/error.rs | 4 ++-- src/gio/file_output_stream.rs | 5 ++++- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index ec29195..b7b2eb9 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -9,12 +9,12 @@ pub use response::Response; // Local dependencies use gio::{ - prelude::{IOStreamExt, OutputStreamExt, OutputStreamExtManual, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, }; use glib::{ object::{Cast, ObjectExt}, - Priority, + Bytes, Priority, }; pub struct Connection { @@ -61,8 +61,11 @@ impl Connection { callback: impl FnOnce(Result<(Response, Self), Error>) + 'static, ) { let output_stream = self.stream().output_stream(); - output_stream.clone().write_async( - request.header().into_bytes(), + // Make sure **all header 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 + output_stream.clone().write_all_async( + Bytes::from_owned(request.header()), priority, Some(&cancellable.clone()), move |result| match result { @@ -78,8 +81,11 @@ impl Connection { }) }, ), - Request::Titan { data, .. } => output_stream.write_bytes_async( - &data, + // 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( + data, priority, Some(&cancellable.clone()), move |result| match result { @@ -94,11 +100,11 @@ impl Connection { }) }, ), - Err(e) => callback(Err(Error::Request(e))), + Err((b, e)) => callback(Err(Error::Request(b, e))), }, ), }, - Err((_, e)) => callback(Err(Error::Request(e))), + Err((b, e)) => callback(Err(Error::Request(b, e))), }, ) } diff --git a/src/client/connection/error.rs b/src/client/connection/error.rs index 178cfba..711c2b6 100644 --- a/src/client/connection/error.rs +++ b/src/client/connection/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Request(glib::Error), + Request(glib::Bytes, glib::Error), Response(crate::client::connection::response::Error), TlsClientConnection(glib::Error), } @@ -10,7 +10,7 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Request(e) => { + Self::Request(_, e) => { write!(f, "Request error: {e}") } Self::Response(e) => { diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 9a62667..9d6ef23 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -44,7 +44,10 @@ pub fn from_stream_async( return on_complete(Ok((file_output_stream, total))); } - file_output_stream.clone().write_async( + // Make sure **all 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 + file_output_stream.clone().write_all_async( bytes.clone(), priority, Some(&cancellable.clone()), From 564f5b69d5605f82387ddc801d61d4ee3842a007 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:50:49 +0200 Subject: [PATCH 34/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 19c3745..39e2fd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.16.1" +version = "0.17.0" edition = "2021" license = "MIT" readme = "README.md" From e3abd89c9d4b2c3382cbbafc8a226dd74a6398ec Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Feb 2025 07:52:43 +0200 Subject: [PATCH 35/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 39e2fd8..9eba89d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.0" +version = "0.17.1" edition = "2021" license = "MIT" readme = "README.md" From 0523f678503768378458a65dd326eef42d76d6b7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 13:16:14 +0200 Subject: [PATCH 36/95] add support for uri starts with double slash --- src/client/connection/response/redirect.rs | 123 +++++++++++------- .../connection/response/redirect/error.rs | 4 + 2 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index add308c..aa5e8e8 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -54,8 +54,32 @@ impl Redirect { // > it is up to the client which fragment to apply. None, // @TODO ) - .parse_relative(self.target(), UriFlags::NONE) - { + .parse_relative( + &{ + // URI started with double slash yet not supported by Glib function + // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + let t = self.target(); + match t.strip_prefix("//") { + Some(p) => { + let postfix = p.trim_start_matches(":"); + format!( + "{}://{}", + base.scheme(), + if postfix.is_empty() { + match base.host() { + Some(h) => format!("{h}/"), + None => return Err(Error::BaseHost), + } + } else { + postfix.to_string() + } + ) + } + None => t.to_string(), + } + }, + UriFlags::NONE, + ) { Ok(absolute) => Ok(absolute), Err(e) => Err(Error::Uri(e)), } @@ -127,50 +151,59 @@ fn target(value: Option<&GStringPtr>) -> Result { } #[test] -fn test_from_str() { +fn test() { use std::str::FromStr; + { + let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); + assert_eq!(temporary.target(), "/uri"); + assert_eq!(temporary.to_code(), TEMPORARY.0); + assert_eq!(temporary.to_string(), TEMPORARY.1); - let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); - assert_eq!(temporary.target(), "/uri"); - assert_eq!(temporary.to_code(), TEMPORARY.0); - assert_eq!(temporary.to_string(), TEMPORARY.1); + let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); + assert_eq!(permanent.target(), "/uri"); + assert_eq!(permanent.to_code(), PERMANENT.0); + assert_eq!(permanent.to_string(), PERMANENT.1); + } + { + let base = Uri::build( + UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); - let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); - assert_eq!(permanent.target(), "/uri"); - assert_eq!(permanent.to_code(), PERMANENT.0); - assert_eq!(permanent.to_string(), PERMANENT.1); -} - -#[test] -fn test_to_uri() { - use std::str::FromStr; - - let request = Uri::build( - UriFlags::NONE, - "gemini", - None, - Some("geminiprotocol.net"), - -1, - "/path/", - Some("query"), - Some("fragment"), - ); - - let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://geminiprotocol.net/uri" - ); - - let resolve = Redirect::from_str("30 uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://geminiprotocol.net/path/uri" - ); - - let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); - assert_eq!( - resolve.to_uri(&request).unwrap().to_string(), - "gemini://test.host/uri" - ); + let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/uri" + ); + + let resolve = Redirect::from_str("30 uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/path/uri" + ); + + let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://test.host/uri" + ); + + let resolve = Redirect::from_str("30 //\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/" + ); + + let resolve = Redirect::from_str("30 //:\r\n").unwrap(); + assert_eq!( + resolve.to_uri(&base).unwrap().to_string(), + "gemini://geminiprotocol.net/" + ); + } } diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index acee073..eeaf2ed 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -5,6 +5,7 @@ use std::{ #[derive(Debug)] pub enum Error { + BaseHost, Uri(glib::Error), Protocol, Target, @@ -14,6 +15,9 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::BaseHost => { + write!(f, "Base host required") + } Self::Uri(e) => { write!(f, "URI error: {e}") } From 06fc69cff8556d42ff79286d7c3194e4b098eb45 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 14:44:30 +0200 Subject: [PATCH 37/95] update dependencies version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9eba89d..8c1d0e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.4" +version = "0.20.9" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.4" +version = "0.20.9" features = ["v2_66"] From d7166dac66d5a3c0bd88483890435f358c444cfa Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 14:49:30 +0200 Subject: [PATCH 38/95] update tests --- src/client/connection/response/redirect.rs | 48 +++++++++++++++------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index aa5e8e8..2554fdf 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -175,34 +175,52 @@ fn test() { Some("query"), Some("fragment"), ); - - let resolve = Redirect::from_str("30 /uri\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 /uri\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/uri" ); - - let resolve = Redirect::from_str("30 uri\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 uri\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/path/uri" ); - - let resolve = Redirect::from_str("30 gemini://test.host/uri\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 gemini://test.host/uri\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://test.host/uri" ); - - let resolve = Redirect::from_str("30 //\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 //\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/" ); - - let resolve = Redirect::from_str("30 //:\r\n").unwrap(); assert_eq!( - resolve.to_uri(&base).unwrap().to_string(), + Redirect::from_str("30 //geminiprotocol.net/path\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), + "gemini://geminiprotocol.net/path" + ); + assert_eq!( + Redirect::from_str("30 //:\r\n") + .unwrap() + .to_uri(&base) + .unwrap() + .to_string(), "gemini://geminiprotocol.net/" ); } From 90cc58ab9286003015270702e76b1cf3e69b93d2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 15 Mar 2025 17:11:44 +0200 Subject: [PATCH 39/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8c1d0e1..b865b2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.1" +version = "0.17.2" edition = "2021" license = "MIT" readme = "README.md" From fc8356f7ac8daef99b87594c1bb4c4593af646f5 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 21:31:22 +0200 Subject: [PATCH 40/95] update rust version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b865b2d..8fc341c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ggemini" version = "0.17.2" -edition = "2021" +edition = "2024" license = "MIT" readme = "README.md" description = "Glib/Gio-oriented network API for Gemini protocol" From e6661c1d00b4fd96c415867246227b99df29bbf7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 21:58:00 +0200 Subject: [PATCH 41/95] apply new fmt version --- src/client.rs | 2 +- src/client/connection.rs | 4 ++-- src/client/connection/response.rs | 2 +- src/gio/file_output_stream.rs | 4 ++-- src/gio/memory_input_stream.rs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index 342eac3..be66b5e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ pub mod error; pub use connection::{Connection, Request, Response}; pub use error::Error; -use gio::{prelude::SocketClientExt, Cancellable, SocketClient, SocketProtocol, TlsCertificate}; +use gio::{Cancellable, SocketClient, SocketProtocol, TlsCertificate, prelude::SocketClientExt}; use glib::Priority; // Defaults diff --git a/src/client/connection.rs b/src/client/connection.rs index b7b2eb9..267077c 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -9,12 +9,12 @@ pub use response::Response; // Local dependencies use gio::{ - prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, + prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, }; use glib::{ - object::{Cast, ObjectExt}, Bytes, Priority, + object::{Cast, ObjectExt}, }; pub struct Connection { diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 51c3c7e..9a16679 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -14,7 +14,7 @@ pub use success::Success; use super::Connection; use gio::{Cancellable, IOStream}; -use glib::{object::IsA, Priority}; +use glib::{Priority, object::IsA}; const HEADER_LEN: usize = 1024; diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 9d6ef23..7d9415c 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -2,10 +2,10 @@ pub mod error; pub use error::Error; use gio::{ - prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, Cancellable, FileOutputStream, IOStream, + prelude::{IOStreamExt, InputStreamExt, OutputStreamExtManual}, }; -use glib::{object::IsA, Bytes, Priority}; +use glib::{Bytes, Priority, object::IsA}; /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index def3845..21c6337 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -2,10 +2,10 @@ pub mod error; pub use error::Error; use gio::{ - prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, Cancellable, IOStream, MemoryInputStream, + prelude::{IOStreamExt, InputStreamExt, MemoryInputStreamExt}, }; -use glib::{object::IsA, Priority}; +use glib::{Priority, object::IsA}; /// Asynchronously create new [MemoryInputStream](https://docs.gtk.org/gio/class.MemoryInputStream.html) /// from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) From 0aeb501760d6a52a695447b420a46ce191fb180f Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 16 Mar 2025 22:01:40 +0200 Subject: [PATCH 42/95] apply new version requirements --- src/client/connection/request.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 46576a3..75a4927 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -39,10 +39,10 @@ impl Request { uri.to_string_partial(UriHideFlags::QUERY), data.len() ); - if let Some(ref mime) = mime { + if let Some(mime) = mime { header.push_str(&format!(";mime={mime}")); } - if let Some(ref token) = token { + if let Some(token) = token { header.push_str(&format!(";token={token}")); } if let Some(query) = uri.query() { From af8a972cca4db570f689edb29fe0373b5f73a984 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 18 Mar 2025 00:48:58 +0200 Subject: [PATCH 43/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8fc341c..54dd4b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.17.2" +version = "0.17.3" edition = "2024" license = "MIT" readme = "README.md" From 5bb52fbd8c5aec9596c02ac641f10606407f0cd2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 01:16:51 +0200 Subject: [PATCH 44/95] hold raw header string --- src/client/connection/response/certificate.rs | 33 +++++++++--- src/client/connection/response/failure.rs | 7 +++ .../connection/response/failure/permanent.rs | 51 +++++++++++++++---- .../connection/response/failure/temporary.rs | 51 +++++++++++++++---- src/client/connection/response/input.rs | 22 ++++++-- src/client/connection/response/redirect.rs | 15 ++++-- src/client/connection/response/success.rs | 13 +++-- 7 files changed, 155 insertions(+), 37 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 160e2f0..e9f76c7 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -9,11 +9,20 @@ const NOT_VALID: (u8, &str) = (11, "Certificate not valid"); /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates pub enum Certificate { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Required { message: Option }, + Required { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized { message: Option }, + NotAuthorized { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid { message: Option }, + NotValid { + header: String, + message: Option, + }, } impl Certificate { @@ -39,11 +48,20 @@ impl Certificate { .0 } + pub fn header(&self) -> &str { + match self { + Self::Required { header, .. } + | Self::NotAuthorized { header, .. } + | Self::NotValid { header, .. } => header, + } + .as_str() + } + pub fn message(&self) -> Option<&str> { match self { - Self::Required { message } => message, - Self::NotAuthorized { message } => message, - Self::NotValid { message } => message, + Self::Required { message, .. } + | Self::NotAuthorized { message, .. } + | Self::NotValid { message, .. } => message, } .as_deref() } @@ -69,16 +87,19 @@ impl std::str::FromStr for Certificate { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("60") { return Ok(Self::Required { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("61") { return Ok(Self::NotAuthorized { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("62") { return Ok(Self::NotValid { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 40c8abf..8e85e84 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -45,6 +45,13 @@ impl Failure { } } + pub fn header(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.header(), + Self::Temporary(temporary) => temporary.header(), + } + } + pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e2ab9e0..e69510c 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -10,15 +10,30 @@ const BAD_REQUEST: (u8, &str) = (59, "bad-request"); /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure pub enum Permanent { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 - Default { message: Option }, + Default { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound { message: Option }, + NotFound { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone { message: Option }, + Gone { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused { message: Option }, + ProxyRequestRefused { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest { message: Option }, + BadRequest { + header: String, + message: Option, + }, } impl Permanent { @@ -46,13 +61,24 @@ impl Permanent { .0 } + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } + | Self::NotFound { header, .. } + | Self::Gone { header, .. } + | Self::ProxyRequestRefused { header, .. } + | Self::BadRequest { header, .. } => header, + } + .as_str() + } + pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::NotFound { message } => message, - Self::Gone { message } => message, - Self::ProxyRequestRefused { message } => message, - Self::BadRequest { message } => message, + Self::Default { message, .. } + | Self::NotFound { message, .. } + | Self::Gone { message, .. } + | Self::ProxyRequestRefused { message, .. } + | Self::BadRequest { message, .. } => message, } .as_deref() } @@ -80,26 +106,31 @@ impl std::str::FromStr for Permanent { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("50") { return Ok(Self::Default { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("51") { return Ok(Self::NotFound { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("52") { return Ok(Self::Gone { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("53") { return Ok(Self::ProxyRequestRefused { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("59") { return Ok(Self::BadRequest { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 768bdcd..c9d8083 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -10,15 +10,30 @@ const SLOW_DOWN: (u8, &str) = (44, "Slow down"); /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure pub enum Temporary { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 - Default { message: Option }, + Default { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { message: Option }, + ServerUnavailable { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { message: Option }, + CgiError { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { message: Option }, + ProxyError { + header: String, + message: Option, + }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { message: Option }, + SlowDown { + header: String, + message: Option, + }, } impl Temporary { @@ -46,13 +61,24 @@ impl Temporary { .0 } + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } + | Self::ServerUnavailable { header, .. } + | Self::CgiError { header, .. } + | Self::ProxyError { header, .. } + | Self::SlowDown { header, .. } => header, + } + .as_str() + } + pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::ServerUnavailable { message } => message, - Self::CgiError { message } => message, - Self::ProxyError { message } => message, - Self::SlowDown { message } => message, + Self::Default { message, .. } + | Self::ServerUnavailable { message, .. } + | Self::CgiError { message, .. } + | Self::ProxyError { message, .. } + | Self::SlowDown { message, .. } => message, } .as_deref() } @@ -80,26 +106,31 @@ impl std::str::FromStr for Temporary { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("40") { return Ok(Self::Default { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("41") { return Ok(Self::ServerUnavailable { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("42") { return Ok(Self::CgiError { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("43") { return Ok(Self::ProxyError { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("44") { return Ok(Self::SlowDown { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index b62276b..c524b9a 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -5,8 +5,14 @@ const DEFAULT: (u8, &str) = (10, "Input"); const SENSITIVE: (u8, &str) = (11, "Sensitive input"); pub enum Input { - Default { message: Option }, - Sensitive { message: Option }, + Default { + header: String, + message: Option, + }, + Sensitive { + header: String, + message: Option, + }, } impl Input { @@ -31,10 +37,16 @@ impl Input { .0 } + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } | Self::Sensitive { header, .. } => header, + } + .as_str() + } + pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::Sensitive { message } => message, + Self::Default { message, .. } | Self::Sensitive { message, .. } => message, } .as_deref() } @@ -59,11 +71,13 @@ impl std::str::FromStr for Input { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("10") { return Ok(Self::Default { + header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("11") { return Ok(Self::Sensitive { + header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 2554fdf..cc838df 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -8,9 +8,9 @@ const PERMANENT: (u8, &str) = (31, "Permanent redirect"); pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Temporary { target: String }, + Temporary { header: String, target: String }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent { target: String }, + Permanent { header: String, target: String }, } impl Redirect { @@ -87,10 +87,15 @@ impl Redirect { // Getters + pub fn header(&self) -> &str { + match self { + Self::Permanent { header, .. } | Self::Temporary { header, .. } => header, + } + } + pub fn target(&self) -> &str { match self { - Self::Permanent { target } => target, - Self::Temporary { target } => target, + Self::Permanent { target, .. } | Self::Temporary { target, .. } => target, } } } @@ -124,9 +129,11 @@ impl std::str::FromStr for Redirect { match regex.get(1) { Some(code) => match code.as_str() { "0" => Ok(Self::Temporary { + header: header.to_string(), target: target(regex.get(2))?, }), "1" => Ok(Self::Permanent { + header: header.to_string(), target: target(regex.get(2))?, }), _ => todo!(), diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index e5ad6f4..f862fd9 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -4,7 +4,7 @@ pub use error::Error; const DEFAULT: (u8, &str) = (20, "Success"); pub enum Success { - Default { mime: String }, + Default { header: String, mime: String }, // reserved for 2* codes } @@ -30,9 +30,16 @@ impl Success { // Getters + pub fn header(&self) -> &str { + match self { + Self::Default { header, .. } => header, + } + .as_str() + } + pub fn mime(&self) -> &str { match self { - Self::Default { mime } => mime, + Self::Default { mime, .. } => mime, } } } @@ -53,7 +60,6 @@ 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, @@ -68,6 +74,7 @@ impl std::str::FromStr for Success { Err(Error::Mime) } else { Ok(Self::Default { + header: header.to_string(), mime: mime.to_lowercase(), }) } From 376473660f12d2e274a0f058df749c90c59f59ca Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 01:17:14 +0200 Subject: [PATCH 45/95] update minor version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From b62f990bf22261fe38aaba88fa4f646169431a34 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:12:43 +0200 Subject: [PATCH 46/95] fix codes, validate header len --- src/client/connection/response/certificate.rs | 10 +++++++--- src/client/connection/response/certificate/error.rs | 10 +++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index e9f76c7..d78c133 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,9 +1,9 @@ pub mod error; pub use error::Error; -const REQUIRED: (u8, &str) = (10, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (11, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (11, "Certificate not valid"); +const REQUIRED: (u8, &str) = (60, "Certificate required"); +const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); +const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); /// 6* status code group /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates @@ -85,6 +85,10 @@ impl std::fmt::Display for Certificate { impl std::str::FromStr for Certificate { type Err = Error; fn from_str(header: &str) -> Result { + let len = header.len(); + if len > super::HEADER_LEN { + return Err(Error::HeaderLen(len)); + } if let Some(postfix) = header.strip_prefix("60") { return Ok(Self::Required { header: header.to_string(), diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs index 5cf1cf6..62f17d1 100644 --- a/src/client/connection/response/certificate/error.rs +++ b/src/client/connection/response/certificate/error.rs @@ -6,6 +6,7 @@ use std::{ #[derive(Debug)] pub enum Error { Code, + HeaderLen(usize), Utf8Error(Utf8Error), } @@ -13,7 +14,14 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { Self::Code => { - write!(f, "Status code error") + write!(f, "Unexpected status code") + } + Self::HeaderLen(l) => { + write!( + f, + "Header length reached protocol limit ({l} of {} bytes max)", + super::super::HEADER_LEN + ) } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") From 6dbf49cea3d4fe5d1caf73cf9c55e3941941c706 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:13:37 +0200 Subject: [PATCH 47/95] validate header len --- src/client/connection/response/success.rs | 47 +++++++++++-------- .../connection/response/success/error.rs | 24 ++++++---- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index f862fd9..fa4c236 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -60,26 +60,35 @@ 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 { - header: header.to_string(), - mime: mime.to_lowercase(), - }) + + if header.len() > super::HEADER_LEN { + return Err(Error::HeaderLen(header.len())); + } + + // * keep separator after code as expected by protocol + match header.strip_prefix("20") { + Some(postfix) => match Regex::split_simple( + r"^\s+([^\/]+\/[^\s;]+)", + postfix, + RegexCompileFlags::DEFAULT, + RegexMatchFlags::DEFAULT, + ) + .get(1) + { + Some(mime) => { + let mime = mime.trim(); + if mime.is_empty() { + Err(Error::ContentType) + } else { + Ok(Self::Default { + header: header.to_string(), + mime: mime.to_lowercase(), + }) + } } - } - None => Err(Error::Protocol), + None => Err(Error::ContentType), + }, + None => Err(Error::Code), } } } diff --git a/src/client/connection/response/success/error.rs b/src/client/connection/response/success/error.rs index 2dbe363..7eaf2e0 100644 --- a/src/client/connection/response/success/error.rs +++ b/src/client/connection/response/success/error.rs @@ -5,23 +5,31 @@ use std::{ #[derive(Debug)] pub enum Error { - Protocol, - Mime, + Code, + ContentType, + HeaderLen(usize), Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::ContentType => { + write!(f, "Content type required") + } + Self::HeaderLen(l) => { + write!( + f, + "Header length reached protocol limit ({l} of {} bytes max)", + super::super::HEADER_LEN + ) + } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } - Self::Protocol => { - write!(f, "Protocol error") - } - Self::Mime => { - write!(f, "MIME error") - } } } } From ab8eb402a87f8131a63530f2e4f1eb55e5ddd9b9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:23:20 +0200 Subject: [PATCH 48/95] decode header bytes only --- src/client/connection/response/certificate.rs | 9 ++++- .../connection/response/failure/permanent.rs | 9 ++++- .../connection/response/failure/temporary.rs | 9 ++++- src/client/connection/response/input.rs | 33 ++++++++++++------- src/client/connection/response/redirect.rs | 9 ++++- src/client/connection/response/success.rs | 9 ++++- 6 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index d78c133..6b9e774 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -31,7 +31,14 @@ impl Certificate { /// 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) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e69510c..e33ea13 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -42,7 +42,14 @@ impl Permanent { /// 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) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::super::HEADER_LEN { + super::super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index c9d8083..21fc2b4 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -42,7 +42,14 @@ impl Temporary { /// 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) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::super::HEADER_LEN { + super::super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index c524b9a..37aa99f 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -21,7 +21,14 @@ impl Input { /// 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) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -69,17 +76,19 @@ impl std::fmt::Display for Input { impl std::str::FromStr for Input { type Err = Error; fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - header: header.to_string(), - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - header: header.to_string(), - message: message(postfix), - }); + if header.len() <= super::HEADER_LEN { + if let Some(postfix) = header.strip_prefix("10") { + return Ok(Self::Default { + header: header.to_string(), + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("11") { + return Ok(Self::Sensitive { + header: header.to_string(), + message: message(postfix), + }); + } } Err(Error::Protocol) } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index cc838df..b936944 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -19,7 +19,14 @@ impl Redirect { /// 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) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index fa4c236..e9c240e 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -14,7 +14,14 @@ impl Success { /// 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) { + let len = buffer.len(); + match std::str::from_utf8( + &buffer[..if len > super::HEADER_LEN { + super::HEADER_LEN + } else { + len + }], + ) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } From 3f968d87b19a386b4e980fb86514760e51568c69 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 03:25:55 +0200 Subject: [PATCH 49/95] update error enum --- src/client/connection/response/input.rs | 29 ++++++++++--------- src/client/connection/response/input/error.rs | 16 +++++++--- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 37aa99f..78aa3a1 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -76,21 +76,22 @@ impl std::fmt::Display for Input { impl std::str::FromStr for Input { type Err = Error; fn from_str(header: &str) -> Result { - if header.len() <= super::HEADER_LEN { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - header: header.to_string(), - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - header: header.to_string(), - message: message(postfix), - }); - } + if header.len() > super::HEADER_LEN { + return Err(Error::HeaderLen(header.len())); } - Err(Error::Protocol) + if let Some(postfix) = header.strip_prefix("10") { + return Ok(Self::Default { + header: header.to_string(), + message: message(postfix), + }); + } + if let Some(postfix) = header.strip_prefix("11") { + return Ok(Self::Sensitive { + header: header.to_string(), + message: message(postfix), + }); + } + Err(Error::Code) } } diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index ae589e8..62f17d1 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -5,19 +5,27 @@ use std::{ #[derive(Debug)] pub enum Error { - Protocol, + Code, + HeaderLen(usize), Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::HeaderLen(l) => { + write!( + f, + "Header length reached protocol limit ({l} of {} bytes max)", + super::super::HEADER_LEN + ) + } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } - Self::Protocol => { - write!(f, "Protocol error") - } } } } From 9eb21bb6a3887a70aaba8239df7e95b2b5765207 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 15:06:53 +0200 Subject: [PATCH 50/95] Revert "hold raw header string" This reverts commit 5bb52fbd8c5aec9596c02ac641f10606407f0cd2. --- Cargo.toml | 2 +- src/client/connection/response/certificate.rs | 52 +++------------ .../connection/response/certificate/error.rs | 10 +-- src/client/connection/response/failure.rs | 7 -- .../connection/response/failure/permanent.rs | 60 ++++------------- .../connection/response/failure/temporary.rs | 60 ++++------------- src/client/connection/response/input.rs | 36 ++-------- src/client/connection/response/input/error.rs | 16 ++--- src/client/connection/response/redirect.rs | 24 ++----- src/client/connection/response/success.rs | 65 ++++++------------- .../connection/response/success/error.rs | 24 +++---- 11 files changed, 78 insertions(+), 278 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 442d0ee..54dd4b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.18.0" +version = "0.17.3" edition = "2024" license = "MIT" readme = "README.md" diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 6b9e774..160e2f0 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,28 +1,19 @@ pub mod error; pub use error::Error; -const REQUIRED: (u8, &str) = (60, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); +const REQUIRED: (u8, &str) = (10, "Certificate required"); +const NOT_AUTHORIZED: (u8, &str) = (11, "Certificate not authorized"); +const NOT_VALID: (u8, &str) = (11, "Certificate not valid"); /// 6* status code group /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates pub enum Certificate { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Required { - header: String, - message: Option, - }, + Required { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized { - header: String, - message: Option, - }, + NotAuthorized { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid { - header: String, - message: Option, - }, + NotValid { message: Option }, } impl Certificate { @@ -31,14 +22,7 @@ impl Certificate { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -55,20 +39,11 @@ impl Certificate { .0 } - pub fn header(&self) -> &str { - match self { - Self::Required { header, .. } - | Self::NotAuthorized { header, .. } - | Self::NotValid { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Required { message, .. } - | Self::NotAuthorized { message, .. } - | Self::NotValid { message, .. } => message, + Self::Required { message } => message, + Self::NotAuthorized { message } => message, + Self::NotValid { message } => message, } .as_deref() } @@ -92,25 +67,18 @@ impl std::fmt::Display for Certificate { impl std::str::FromStr for Certificate { type Err = Error; fn from_str(header: &str) -> Result { - let len = header.len(); - if len > super::HEADER_LEN { - return Err(Error::HeaderLen(len)); - } if let Some(postfix) = header.strip_prefix("60") { return Ok(Self::Required { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("61") { return Ok(Self::NotAuthorized { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("62") { return Ok(Self::NotValid { - header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs index 62f17d1..5cf1cf6 100644 --- a/src/client/connection/response/certificate/error.rs +++ b/src/client/connection/response/certificate/error.rs @@ -6,7 +6,6 @@ use std::{ #[derive(Debug)] pub enum Error { Code, - HeaderLen(usize), Utf8Error(Utf8Error), } @@ -14,14 +13,7 @@ impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { Self::Code => { - write!(f, "Unexpected status code") - } - Self::HeaderLen(l) => { - write!( - f, - "Header length reached protocol limit ({l} of {} bytes max)", - super::super::HEADER_LEN - ) + write!(f, "Status code error") } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 8e85e84..40c8abf 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -45,13 +45,6 @@ impl Failure { } } - pub fn header(&self) -> &str { - match self { - Self::Permanent(permanent) => permanent.header(), - Self::Temporary(temporary) => temporary.header(), - } - } - pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e33ea13..e2ab9e0 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -10,30 +10,15 @@ const BAD_REQUEST: (u8, &str) = (59, "bad-request"); /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure pub enum Permanent { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 - Default { - header: String, - message: Option, - }, + Default { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound { - header: String, - message: Option, - }, + NotFound { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone { - header: String, - message: Option, - }, + Gone { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused { - header: String, - message: Option, - }, + ProxyRequestRefused { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest { - header: String, - message: Option, - }, + BadRequest { message: Option }, } impl Permanent { @@ -42,14 +27,7 @@ impl Permanent { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::super::HEADER_LEN { - super::super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -68,24 +46,13 @@ impl Permanent { .0 } - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } - | Self::NotFound { header, .. } - | Self::Gone { header, .. } - | Self::ProxyRequestRefused { header, .. } - | Self::BadRequest { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message, .. } - | Self::NotFound { message, .. } - | Self::Gone { message, .. } - | Self::ProxyRequestRefused { message, .. } - | Self::BadRequest { message, .. } => message, + Self::Default { message } => message, + Self::NotFound { message } => message, + Self::Gone { message } => message, + Self::ProxyRequestRefused { message } => message, + Self::BadRequest { message } => message, } .as_deref() } @@ -113,31 +80,26 @@ impl std::str::FromStr for Permanent { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("50") { return Ok(Self::Default { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("51") { return Ok(Self::NotFound { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("52") { return Ok(Self::Gone { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("53") { return Ok(Self::ProxyRequestRefused { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("59") { return Ok(Self::BadRequest { - header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 21fc2b4..768bdcd 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -10,30 +10,15 @@ const SLOW_DOWN: (u8, &str) = (44, "Slow down"); /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure pub enum Temporary { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 - Default { - header: String, - message: Option, - }, + Default { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { - header: String, - message: Option, - }, + ServerUnavailable { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { - header: String, - message: Option, - }, + CgiError { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { - header: String, - message: Option, - }, + ProxyError { message: Option }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { - header: String, - message: Option, - }, + SlowDown { message: Option }, } impl Temporary { @@ -42,14 +27,7 @@ impl Temporary { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::super::HEADER_LEN { - super::super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -68,24 +46,13 @@ impl Temporary { .0 } - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } - | Self::ServerUnavailable { header, .. } - | Self::CgiError { header, .. } - | Self::ProxyError { header, .. } - | Self::SlowDown { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message, .. } - | Self::ServerUnavailable { message, .. } - | Self::CgiError { message, .. } - | Self::ProxyError { message, .. } - | Self::SlowDown { message, .. } => message, + Self::Default { message } => message, + Self::ServerUnavailable { message } => message, + Self::CgiError { message } => message, + Self::ProxyError { message } => message, + Self::SlowDown { message } => message, } .as_deref() } @@ -113,31 +80,26 @@ impl std::str::FromStr for Temporary { fn from_str(header: &str) -> Result { if let Some(postfix) = header.strip_prefix("40") { return Ok(Self::Default { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("41") { return Ok(Self::ServerUnavailable { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("42") { return Ok(Self::CgiError { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("43") { return Ok(Self::ProxyError { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("44") { return Ok(Self::SlowDown { - header: header.to_string(), message: message(postfix), }); } diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 78aa3a1..b62276b 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -5,14 +5,8 @@ const DEFAULT: (u8, &str) = (10, "Input"); const SENSITIVE: (u8, &str) = (11, "Sensitive input"); pub enum Input { - Default { - header: String, - message: Option, - }, - Sensitive { - header: String, - message: Option, - }, + Default { message: Option }, + Sensitive { message: Option }, } impl Input { @@ -21,14 +15,7 @@ impl Input { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -44,16 +31,10 @@ impl Input { .0 } - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } | Self::Sensitive { header, .. } => header, - } - .as_str() - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message, .. } | Self::Sensitive { message, .. } => message, + Self::Default { message } => message, + Self::Sensitive { message } => message, } .as_deref() } @@ -76,22 +57,17 @@ impl std::fmt::Display for Input { impl std::str::FromStr for Input { type Err = Error; fn from_str(header: &str) -> Result { - if header.len() > super::HEADER_LEN { - return Err(Error::HeaderLen(header.len())); - } if let Some(postfix) = header.strip_prefix("10") { return Ok(Self::Default { - header: header.to_string(), message: message(postfix), }); } if let Some(postfix) = header.strip_prefix("11") { return Ok(Self::Sensitive { - header: header.to_string(), message: message(postfix), }); } - Err(Error::Code) + Err(Error::Protocol) } } diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index 62f17d1..ae589e8 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -5,27 +5,19 @@ use std::{ #[derive(Debug)] pub enum Error { - Code, - HeaderLen(usize), + Protocol, Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Unexpected status code") - } - Self::HeaderLen(l) => { - write!( - f, - "Header length reached protocol limit ({l} of {} bytes max)", - super::super::HEADER_LEN - ) - } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } + Self::Protocol => { + write!(f, "Protocol error") + } } } } diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index b936944..2554fdf 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -8,9 +8,9 @@ const PERMANENT: (u8, &str) = (31, "Permanent redirect"); pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Temporary { header: String, target: String }, + Temporary { target: String }, /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent { header: String, target: String }, + Permanent { target: String }, } impl Redirect { @@ -19,14 +19,7 @@ impl Redirect { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -94,15 +87,10 @@ impl Redirect { // Getters - pub fn header(&self) -> &str { - match self { - Self::Permanent { header, .. } | Self::Temporary { header, .. } => header, - } - } - pub fn target(&self) -> &str { match self { - Self::Permanent { target, .. } | Self::Temporary { target, .. } => target, + Self::Permanent { target } => target, + Self::Temporary { target } => target, } } } @@ -136,11 +124,9 @@ impl std::str::FromStr for Redirect { match regex.get(1) { Some(code) => match code.as_str() { "0" => Ok(Self::Temporary { - header: header.to_string(), target: target(regex.get(2))?, }), "1" => Ok(Self::Permanent { - header: header.to_string(), target: target(regex.get(2))?, }), _ => todo!(), diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index e9c240e..e5ad6f4 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -4,7 +4,7 @@ pub use error::Error; const DEFAULT: (u8, &str) = (20, "Success"); pub enum Success { - Default { header: String, mime: String }, + Default { mime: String }, // reserved for 2* codes } @@ -14,14 +14,7 @@ impl Success { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { use std::str::FromStr; - let len = buffer.len(); - match std::str::from_utf8( - &buffer[..if len > super::HEADER_LEN { - super::HEADER_LEN - } else { - len - }], - ) { + match std::str::from_utf8(buffer) { Ok(header) => Self::from_str(header), Err(e) => Err(Error::Utf8Error(e)), } @@ -37,16 +30,9 @@ impl Success { // Getters - pub fn header(&self) -> &str { - match self { - Self::Default { header, .. } => header, - } - .as_str() - } - pub fn mime(&self) -> &str { match self { - Self::Default { mime, .. } => mime, + Self::Default { mime } => mime, } } } @@ -68,34 +54,25 @@ impl std::str::FromStr for Success { fn from_str(header: &str) -> Result { use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; - if header.len() > super::HEADER_LEN { - return Err(Error::HeaderLen(header.len())); - } - - // * keep separator after code as expected by protocol - match header.strip_prefix("20") { - Some(postfix) => match Regex::split_simple( - r"^\s+([^\/]+\/[^\s;]+)", - postfix, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ) - .get(1) - { - Some(mime) => { - let mime = mime.trim(); - if mime.is_empty() { - Err(Error::ContentType) - } else { - Ok(Self::Default { - header: header.to_string(), - mime: mime.to_lowercase(), - }) - } + 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::ContentType), - }, - None => Err(Error::Code), + } + None => Err(Error::Protocol), } } } diff --git a/src/client/connection/response/success/error.rs b/src/client/connection/response/success/error.rs index 7eaf2e0..2dbe363 100644 --- a/src/client/connection/response/success/error.rs +++ b/src/client/connection/response/success/error.rs @@ -5,31 +5,23 @@ use std::{ #[derive(Debug)] pub enum Error { - Code, - ContentType, - HeaderLen(usize), + Protocol, + Mime, Utf8Error(Utf8Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Unexpected status code") - } - Self::ContentType => { - write!(f, "Content type required") - } - Self::HeaderLen(l) => { - write!( - f, - "Header length reached protocol limit ({l} of {} bytes max)", - super::super::HEADER_LEN - ) - } Self::Utf8Error(e) => { write!(f, "UTF-8 error: {e}") } + Self::Protocol => { + write!(f, "Protocol error") + } + Self::Mime => { + write!(f, "MIME error") + } } } } From 2102d8887afeedbf6156cd8b6a89bfed12bcf0e4 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 19 Mar 2025 15:07:22 +0200 Subject: [PATCH 51/95] fix codes --- src/client/connection/response/certificate.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 160e2f0..423d0e8 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,9 +1,9 @@ pub mod error; pub use error::Error; -const REQUIRED: (u8, &str) = (10, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (11, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (11, "Certificate not valid"); +const REQUIRED: (u8, &str) = (60, "Certificate required"); +const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); +const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); /// 6* status code group /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates From a12a73d31175f9f34d51dc06aa1b8ec2684ac359 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sat, 22 Mar 2025 19:03:42 +0200 Subject: [PATCH 52/95] hold `NetworkAddress` and `SocketConnection` as the `Connection` members --- src/client.rs | 2 +- src/client/connection.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index be66b5e..cff557e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -74,8 +74,8 @@ impl Client { Ok(socket_connection) => { match Connection::build( socket_connection, + network_address, certificate, - Some(network_address), is_session_resumption, ) { Ok(connection) => connection.request_async( diff --git a/src/client/connection.rs b/src/client/connection.rs index 267077c..028e00e 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -18,6 +18,8 @@ use glib::{ }; pub struct Connection { + pub network_address: NetworkAddress, + pub socket_connection: SocketConnection, pub tls_client_connection: TlsClientConnection, } @@ -27,24 +29,26 @@ impl Connection { /// Create new `Self` pub fn build( socket_connection: SocketConnection, + network_address: NetworkAddress, certificate: Option, - server_identity: Option, is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, - server_identity.as_ref(), + Some(&network_address), is_session_resumption, ) { Ok(tls_client_connection) => { - if let Some(ref certificate) = certificate { - tls_client_connection.set_certificate(certificate); + if let Some(ref c) = certificate { + tls_client_connection.set_certificate(c); } tls_client_connection } Err(e) => return Err(e), }, + network_address, + socket_connection, }) } From 7c518cecf6be56d8b917b10c5131b0ef7dcf3377 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 06:50:08 +0200 Subject: [PATCH 53/95] 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}") } } } From 0717e473b7d6707fdeefcc8c476b2e0670e49dc6 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:06:54 +0200 Subject: [PATCH 54/95] remove unsupported modes, add comments --- src/client/connection.rs | 2 -- src/client/connection/request/mode.rs | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/connection.rs b/src/client/connection.rs index 8bea8b2..c62a874 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -75,7 +75,6 @@ impl Connection { move |result| match result { Ok(_) => match request { Request::Gemini { mode, .. } => match mode { - Mode::All => todo!(), Mode::Header => Response::header_from_connection_async( self, priority, @@ -97,7 +96,6 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match mode { - Mode::All => todo!(), Mode::Header => Response::header_from_connection_async( self, priority, diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs index 6713bbd..0d006ef 100644 --- a/src/client/connection/request/mode.rs +++ b/src/client/connection/request/mode.rs @@ -1,4 +1,6 @@ +/// Request modes pub enum Mode { + /// Request header bytes only, process content bytes manually + /// * useful for manual content type handle: text, stream or large content loaded by chunks Header, - All, } From 68e789412597f94e81aaa882cf0cd81b2608167a Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:10:22 +0200 Subject: [PATCH 55/95] apply clippy --- src/client/connection/response/success.rs | 4 ++-- src/client/connection/response/success/default.rs | 2 +- src/client/connection/response/success/default/header.rs | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 591510d..6ead866 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -16,10 +16,10 @@ impl Success { /// Parse new `Self` from buffer bytes pub fn parse(buffer: &[u8]) -> Result { - if !buffer.first().is_some_and(|b| *b == CODE) { + if buffer.first().is_none_or(|b| *b != CODE) { return Err(Error::Code); } - match Default::parse(&buffer) { + match Default::parse(buffer) { Ok(default) => Ok(Self::Default(default)), Err(e) => Err(Error::Default(e)), } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 34f9cb0..90ac8f5 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -18,7 +18,7 @@ impl Default { if !buffer.starts_with(CODE) { return Err(Error::Code); } - let header = Header::parse(buffer).map_err(|e| Error::Header(e))?; + let header = Header::parse(buffer).map_err(Error::Header)?; Ok(Self { content: buffer.get(header.len() + 1..).map(|v| v.to_vec()), header, diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs index ef326bc..306a984 100644 --- a/src/client/connection/response/success/default/header.rs +++ b/src/client/connection/response/success/default/header.rs @@ -12,7 +12,7 @@ impl Header { } Ok(Self( crate::client::connection::response::header_bytes(buffer) - .map_err(|e| Error::Header(e))? + .map_err(Error::Header)? .to_vec(), )) } @@ -23,7 +23,7 @@ impl Header { 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))?, + std::str::from_utf8(&self.0).map_err(Error::Utf8Error)?, glib::RegexCompileFlags::DEFAULT, glib::RegexMatchFlags::DEFAULT, ) @@ -37,6 +37,10 @@ impl Header { self.0.len() } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn as_bytes(&self) -> &[u8] { &self.0 } From 3de096ccedc215408979f2efb0b7ce984f574233 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:35:57 +0200 Subject: [PATCH 56/95] add tests --- src/client/connection/response/success.rs | 9 +++++++-- src/client/connection/response/success/default.rs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 6ead866..6316864 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -28,6 +28,11 @@ impl Success { #[test] fn test() { - // let default = Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()); - todo!() + match Success::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap() + { + Success::Default(default) => { + assert_eq!(default.header.mime().unwrap(), "text/gemini"); + assert_eq!(default.content, None) + } + } } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 90ac8f5..5d629b8 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -20,8 +20,19 @@ impl Default { } let header = Header::parse(buffer).map_err(Error::Header)?; Ok(Self { - content: buffer.get(header.len() + 1..).map(|v| v.to_vec()), + content: buffer + .get(header.len() + 1..) + .filter(|s| !s.is_empty()) + .map(|v| v.to_vec()), header, }) } } + +#[test] +fn test() { + let default = + Default::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap(); + assert_eq!(default.header.mime().unwrap(), "text/gemini"); + assert_eq!(default.content, None) +} From 71043bbf7375c9384ce239e629d9897d65094b47 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:38:26 +0200 Subject: [PATCH 57/95] remove extra format --- src/client/connection/response/success.rs | 3 +-- src/client/connection/response/success/default.rs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 6316864..a46e141 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -28,8 +28,7 @@ impl Success { #[test] fn test() { - match Success::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap() - { + match Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { Success::Default(default) => { assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 5d629b8..ae57dca 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -31,8 +31,7 @@ impl Default { #[test] fn test() { - let default = - Default::parse(format!("20 text/gemini; charset=utf-8; lang=en\r\n").as_bytes()).unwrap(); + let default = Default::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) } From 68277f8e8305ad7eb85c38476e7c9c6965b18468 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 07:43:44 +0200 Subject: [PATCH 58/95] update example --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c9e3c23..622e680 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,11 @@ fn main() -> ExitCode { None, // optional `GTlsCertificate` |result| match result { Ok((response, _connection)) => match response { - Response::Success(success) => match success.mime() { - "text/gemini" => todo!(), - _ => todo!(), + Response::Success(success) => match success { + Success::Default(default) => match default.header.mime().unwrap().as_str() { + "text/gemini" => todo!(), + _ => todo!(), + } }, _ => todo!(), }, From 5360c6bf19337ed74493b451b96ad002e1488a0f Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 18:31:55 +0200 Subject: [PATCH 59/95] close code members --- src/client/connection/response/success.rs | 2 +- src/client/connection/response/success/default.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index a46e141..2139950 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -4,7 +4,7 @@ pub mod error; pub use default::Default; pub use error::Error; -pub const CODE: u8 = b'2'; +const CODE: u8 = b'2'; pub enum Success { Default(Default), diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index ae57dca..7544723 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -4,7 +4,7 @@ pub mod header; pub use error::Error; pub use header::Header; -pub const CODE: &[u8] = b"20"; +const CODE: &[u8] = b"20"; pub struct Default { pub header: Header, From 8feab6b93bc276685b160081a3b7a4948b384596 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 19:40:12 +0200 Subject: [PATCH 60/95] rename constructors --- src/client/connection/response.rs | 2 +- src/client/connection/response/success.rs | 6 +++--- src/client/connection/response/success/default.rs | 7 ++++--- src/client/connection/response/success/default/header.rs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 98fe2e3..a92e6e3 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -49,7 +49,7 @@ impl Response { Ok(input) => Ok(Self::Input(input)), Err(e) => Err(Error::Input(e)), }, - b'2' => match Success::parse(&buffer) { + b'2' => match Success::from_utf8(&buffer) { Ok(success) => Ok(Self::Success(success)), Err(e) => Err(Error::Success(e)), }, diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index 2139950..ecee769 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -15,11 +15,11 @@ impl Success { // Constructors /// Parse new `Self` from buffer bytes - pub fn parse(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { if buffer.first().is_none_or(|b| *b != CODE) { return Err(Error::Code); } - match Default::parse(buffer) { + match Default::from_utf8(buffer) { Ok(default) => Ok(Self::Default(default)), Err(e) => Err(Error::Default(e)), } @@ -28,7 +28,7 @@ impl Success { #[test] fn test() { - match Success::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { + match Success::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { Success::Default(default) => { assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 7544723..6180f2e 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -14,11 +14,11 @@ pub struct Default { impl Default { // Constructors - pub fn parse(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(CODE) { return Err(Error::Code); } - let header = Header::parse(buffer).map_err(Error::Header)?; + let header = Header::from_utf8(buffer).map_err(Error::Header)?; Ok(Self { content: buffer .get(header.len() + 1..) @@ -31,7 +31,8 @@ impl Default { #[test] fn test() { - let default = Default::parse("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); + let default = + Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); assert_eq!(default.header.mime().unwrap(), "text/gemini"); assert_eq!(default.content, None) } diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs index 306a984..929cb96 100644 --- a/src/client/connection/response/success/default/header.rs +++ b/src/client/connection/response/success/default/header.rs @@ -6,7 +6,7 @@ pub struct Header(Vec); impl Header { // Constructors - pub fn parse(buffer: &[u8]) -> Result { + pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(super::CODE) { return Err(Error::Code); } From 161142c8090f0a3a836ecff4710468d0eff928a3 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 19:57:54 +0200 Subject: [PATCH 61/95] rename mode const --- README.md | 2 +- src/client/connection.rs | 4 ++-- src/client/connection/request.rs | 4 ++-- src/client/connection/request/mode.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 622e680..3abf80f 100644 --- a/README.md +++ b/README.md @@ -51,7 +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) + mode: Mode::HeaderOnly // handle content separately (based on MIME) }, Priority::DEFAULT, Cancellable::new(), diff --git a/src/client/connection.rs b/src/client/connection.rs index c62a874..d1cd849 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -75,7 +75,7 @@ impl Connection { move |result| match result { Ok(_) => match request { Request::Gemini { mode, .. } => match mode { - Mode::Header => Response::header_from_connection_async( + Mode::HeaderOnly => Response::header_from_connection_async( self, priority, cancellable, @@ -96,7 +96,7 @@ impl Connection { Some(&cancellable.clone()), move |result| match result { Ok(_) => match mode { - Mode::Header => Response::header_from_connection_async( + Mode::HeaderOnly => Response::header_from_connection_async( self, priority, cancellable, diff --git a/src/client/connection/request.rs b/src/client/connection/request.rs index 83632ca..f67238b 100644 --- a/src/client/connection/request.rs +++ b/src/client/connection/request.rs @@ -86,7 +86,7 @@ fn test_gemini_header() { assert_eq!( Request::Gemini { uri: Uri::parse(REQUEST, UriFlags::NONE).unwrap(), - mode: Mode::Header + mode: Mode::HeaderOnly } .header(), format!("{REQUEST}\r\n") @@ -111,7 +111,7 @@ fn test_titan_header() { data: Bytes::from(DATA), mime: Some(MIME.to_string()), token: Some(TOKEN.to_string()), - mode: Mode::Header + mode: Mode::HeaderOnly } .header(), format!( diff --git a/src/client/connection/request/mode.rs b/src/client/connection/request/mode.rs index 0d006ef..b1d8a67 100644 --- a/src/client/connection/request/mode.rs +++ b/src/client/connection/request/mode.rs @@ -2,5 +2,5 @@ pub enum Mode { /// Request header bytes only, process content bytes manually /// * useful for manual content type handle: text, stream or large content loaded by chunks - Header, + HeaderOnly, } From a32eccf5cb4d50ac5373f8373838623cc772875e Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 20:46:54 +0200 Subject: [PATCH 62/95] reorganize input format: make constructors lazy, parse members on get --- src/client/connection/response/input.rs | 127 +++++++----------- .../connection/response/input/default.rs | 61 +++++++++ .../response/input/default/error.rs | 24 ++++ src/client/connection/response/input/error.rs | 31 ++++- .../connection/response/input/sensitive.rs | 61 +++++++++ .../response/input/sensitive/error.rs | 24 ++++ 6 files changed, 243 insertions(+), 85 deletions(-) create mode 100644 src/client/connection/response/input/default.rs create mode 100644 src/client/connection/response/input/default/error.rs create mode 100644 src/client/connection/response/input/sensitive.rs create mode 100644 src/client/connection/response/input/sensitive/error.rs diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index b62276b..81988ff 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -1,12 +1,17 @@ +pub mod default; pub mod error; +pub mod sensitive; + +pub use default::Default; pub use error::Error; +pub use sensitive::Sensitive; -const DEFAULT: (u8, &str) = (10, "Input"); -const SENSITIVE: (u8, &str) = (11, "Sensitive input"); +const CODE: u8 = b'1'; +/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected) pub enum Input { - Default { message: Option }, - Sensitive { message: Option }, + Default(Default), + Sensitive(Sensitive), } impl Input { @@ -14,97 +19,63 @@ impl Input { /// 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)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::Sensitive( + Sensitive::from_utf8(buffer).map_err(Error::Sensitive)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Default { .. } => DEFAULT, - Self::Sensitive { .. } => SENSITIVE, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::Sensitive { message } => message, + Self::Default(default) => default.message(), + Self::Sensitive(sensitive) => sensitive.message(), } - .as_deref() } -} -impl std::fmt::Display for Input { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Default { .. } => DEFAULT, - Self::Sensitive { .. } => SENSITIVE, - } - .1 - ) - } -} - -impl std::str::FromStr for Input { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - message: message(postfix), - }); + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::Sensitive(sensitive) => sensitive.as_str(), } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - message: message(postfix), - }); - } - Err(Error::Protocol) } -} -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::Sensitive(sensitive) => sensitive.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - +fn test() { + fn t(source: &str, message: Option<&str>) { + let bytes = source.as_bytes(); + let input = Input::from_utf8(bytes).unwrap(); + assert_eq!(input.message(), message); + assert_eq!(input.as_str(), source); + assert_eq!(input.as_bytes(), bytes); + } // 10 - let default = Input::from_str("10 Default\r\n").unwrap(); - assert_eq!(default.message(), Some("Default")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Input::from_str("10\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - + t("10 Default\r\n", Some("Default")); + t("10\r\n", None); // 11 - let sensitive = Input::from_str("11 Sensitive\r\n").unwrap(); - assert_eq!(sensitive.message(), Some("Sensitive")); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); - - let sensitive = Input::from_str("11\r\n").unwrap(); - assert_eq!(sensitive.message(), None); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); + t("11 Sensitive\r\n", Some("Sensitive")); + t("11\r\n", None); } diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs new file mode 100644 index 0000000..f0a04f3 --- /dev/null +++ b/src/client/connection/response/input/default.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"10"; + +/// Hold header `String` for [10](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); + assert_eq!(default.message(), Some("Default")); + assert_eq!(default.as_str(), "10 Default\r\n"); + + let default = Default::from_utf8("10\r\n".as_bytes()).unwrap(); + assert_eq!(default.message(), None); + assert_eq!(default.as_str(), "10\r\n"); + + // err + assert!(Default::from_utf8("12 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/input/default/error.rs b/src/client/connection/response/input/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index ae589e8..bcb7f8a 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -1,23 +1,40 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { + Default(super::default::Error), + FirstByte(u8), Protocol, - Utf8Error(Utf8Error), + SecondByte(u8), + Sensitive(super::sensitive::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } Self::Protocol => { write!(f, "Protocol error") } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::Sensitive(e) => { + write!(f, "Sensitive parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") + } } } } diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs new file mode 100644 index 0000000..d456490 --- /dev/null +++ b/src/client/connection/response/input/sensitive.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"11"; + +/// Hold header `String` for [11](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Sensitive(String); + +impl Sensitive { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); + assert_eq!(sensitive.message(), Some("Sensitive")); + assert_eq!(sensitive.as_str(), "11 Sensitive\r\n"); + + let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); + assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.as_str(), "11\r\n"); + + // err + assert!(Sensitive::from_utf8("12 Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/input/sensitive/error.rs b/src/client/connection/response/input/sensitive/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/sensitive/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From e94923ecb55b6e9e715562137dbf50b4e9870119 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 20:49:33 +0200 Subject: [PATCH 63/95] fix last byte inclusion --- src/client/connection/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index a92e6e3..840cf08 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -137,7 +137,7 @@ fn header_bytes(buffer: &[u8]) -> Result<&[u8], HeaderBytesError> { if *b == b'\r' { let n = i + 1; if buffer.get(n).is_some_and(|b| *b == b'\n') { - return Ok(&buffer[..n]); + return Ok(&buffer[..n + 1]); } break; } From 845f3dc77e3b348ad9e3778caf6c12eac2cae60b Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:36:00 +0200 Subject: [PATCH 64/95] enshort var names --- src/client/connection/response/input.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 81988ff..549b13e 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -66,11 +66,11 @@ impl Input { #[test] fn test() { fn t(source: &str, message: Option<&str>) { - let bytes = source.as_bytes(); - let input = Input::from_utf8(bytes).unwrap(); - assert_eq!(input.message(), message); - assert_eq!(input.as_str(), source); - assert_eq!(input.as_bytes(), bytes); + let b = source.as_bytes(); + let i = Input::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); } // 10 t("10 Default\r\n", Some("Default")); From 1b96270598c0d7b19564b2f8eb28782883e0c7c7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:38:07 +0200 Subject: [PATCH 65/95] remove deprecated enum values --- src/client/connection/response/input/error.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index bcb7f8a..6763727 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -4,7 +4,6 @@ use std::fmt::{Display, Formatter, Result}; pub enum Error { Default(super::default::Error), FirstByte(u8), - Protocol, SecondByte(u8), Sensitive(super::sensitive::Error), UndefinedFirstByte, @@ -20,9 +19,6 @@ impl Display for Error { Self::FirstByte(b) => { write!(f, "Unexpected first byte: {b}") } - Self::Protocol => { - write!(f, "Protocol error") - } Self::SecondByte(b) => { write!(f, "Unexpected second byte: {b}") } From 232531a0bc403668b70c0415596688404a91d973 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:50:03 +0200 Subject: [PATCH 66/95] reorganize certificate structs format: make constructors lazy, parse members on get --- src/client/connection/response/certificate.rs | 144 ++++++++---------- .../connection/response/certificate/error.rs | 37 +++-- .../response/certificate/not_authorized.rs | 61 ++++++++ .../certificate/not_authorized/error.rs | 24 +++ .../response/certificate/not_valid.rs | 61 ++++++++ .../response/certificate/not_valid/error.rs | 24 +++ .../response/certificate/required.rs | 61 ++++++++ .../response/certificate/required/error.rs | 24 +++ 8 files changed, 345 insertions(+), 91 deletions(-) create mode 100644 src/client/connection/response/certificate/not_authorized.rs create mode 100644 src/client/connection/response/certificate/not_authorized/error.rs create mode 100644 src/client/connection/response/certificate/not_valid.rs create mode 100644 src/client/connection/response/certificate/not_valid/error.rs create mode 100644 src/client/connection/response/certificate/required.rs create mode 100644 src/client/connection/response/certificate/required/error.rs diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 423d0e8..e67ded4 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,19 +1,24 @@ pub mod error; -pub use error::Error; +pub mod not_authorized; +pub mod not_valid; +pub mod required; -const REQUIRED: (u8, &str) = (60, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); +pub use error::Error; +pub use not_authorized::NotAuthorized; +pub use not_valid::NotValid; +pub use required::Required; + +const CODE: u8 = b'6'; /// 6* status code group /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates pub enum Certificate { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Required { message: Option }, + Required(Required), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized { message: Option }, + NotAuthorized(NotAuthorized), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid { message: Option }, + NotValid(NotValid), } impl Certificate { @@ -21,95 +26,72 @@ impl Certificate { /// 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)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Required( + Required::from_utf8(buffer).map_err(Error::Required)?, + )), + b'1' => Ok(Self::NotAuthorized( + NotAuthorized::from_utf8(buffer).map_err(Error::NotAuthorized)?, + )), + b'2' => Ok(Self::NotValid( + NotValid::from_utf8(buffer).map_err(Error::NotValid)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Required { .. } => REQUIRED, - Self::NotAuthorized { .. } => NOT_AUTHORIZED, - Self::NotValid { .. } => NOT_VALID, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Required { message } => message, - Self::NotAuthorized { message } => message, - Self::NotValid { message } => message, + Self::Required(required) => required.message(), + Self::NotAuthorized(not_authorized) => not_authorized.message(), + Self::NotValid(not_valid) => not_valid.message(), } - .as_deref() } -} -impl std::fmt::Display for Certificate { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Required { .. } => REQUIRED, - Self::NotAuthorized { .. } => NOT_AUTHORIZED, - Self::NotValid { .. } => NOT_VALID, - } - .1 - ) + pub fn as_str(&self) -> &str { + match self { + Self::Required(required) => required.as_str(), + Self::NotAuthorized(not_authorized) => not_authorized.as_str(), + Self::NotValid(not_valid) => not_valid.as_str(), + } } -} -impl std::str::FromStr for Certificate { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("60") { - return Ok(Self::Required { - message: message(postfix), - }); + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Required(required) => required.as_bytes(), + Self::NotAuthorized(not_authorized) => not_authorized.as_bytes(), + Self::NotValid(not_valid) => not_valid.as_bytes(), } - if let Some(postfix) = header.strip_prefix("61") { - return Ok(Self::NotAuthorized { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("62") { - return Ok(Self::NotValid { - message: message(postfix), - }); - } - Err(Error::Code) - } -} - -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) } } #[test] -fn test_from_str() { - use std::str::FromStr; - - let required = Certificate::from_str("60 Message\r\n").unwrap(); - - assert_eq!(required.message(), Some("Message")); - assert_eq!(required.to_code(), REQUIRED.0); - assert_eq!(required.to_string(), REQUIRED.1); - - let required = Certificate::from_str("60\r\n").unwrap(); - - assert_eq!(required.message(), None); - assert_eq!(required.to_code(), REQUIRED.0); - assert_eq!(required.to_string(), REQUIRED.1); +fn test() { + fn t(source: &str, message: Option<&str>) { + let b = source.as_bytes(); + let c = Certificate::from_utf8(b).unwrap(); + assert_eq!(c.message(), message); + assert_eq!(c.as_str(), source); + assert_eq!(c.as_bytes(), b); + } + // 60 + t("60 Required\r\n", Some("Required")); + t("60\r\n", None); + // 61 + t("61 Not Authorized\r\n", Some("Not Authorized")); + t("61\r\n", None); + // 62 + t("61 Not Valid\r\n", Some("Not Valid")); + t("61\r\n", None); } diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs index 5cf1cf6..a710617 100644 --- a/src/client/connection/response/certificate/error.rs +++ b/src/client/connection/response/certificate/error.rs @@ -1,22 +1,39 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + FirstByte(u8), + NotAuthorized(super::not_authorized::Error), + NotValid(super::not_valid::Error), + Required(super::required::Error), + SecondByte(u8), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::NotAuthorized(e) => { + write!(f, "NotAuthorized status parse error: {e}") + } + Self::NotValid(e) => { + write!(f, "NotValid status parse error: {e}") + } + Self::Required(e) => { + write!(f, "Required status parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs new file mode 100644 index 0000000..fe85d1e --- /dev/null +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"61"; + +/// Hold header `String` for [61](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotAuthorized(String); + +impl NotAuthorized { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let not_authorized = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); + assert_eq!(not_authorized.message(), Some("Not Authorized")); + assert_eq!(not_authorized.as_str(), "61 Not Authorized\r\n"); + + let not_authorized = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); + assert_eq!(not_authorized.message(), None); + assert_eq!(not_authorized.as_str(), "61\r\n"); + + // err + assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/not_authorized/error.rs b/src/client/connection/response/certificate/not_authorized/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/not_authorized/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs new file mode 100644 index 0000000..35ad475 --- /dev/null +++ b/src/client/connection/response/certificate/not_valid.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"62"; + +/// Hold header `String` for [62](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotValid(String); + +impl NotValid { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let not_valid = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); + assert_eq!(not_valid.message(), Some("Not Valid")); + assert_eq!(not_valid.as_str(), "62 Not Valid\r\n"); + + let not_valid = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); + assert_eq!(not_valid.message(), None); + assert_eq!(not_valid.as_str(), "62\r\n"); + + // err + // @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("63 Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/not_valid/error.rs b/src/client/connection/response/certificate/not_valid/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/not_valid/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs new file mode 100644 index 0000000..df0ef63 --- /dev/null +++ b/src/client/connection/response/certificate/required.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"60"; + +/// Hold header `String` for [60](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Required(String); + +impl Required { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let required = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); + assert_eq!(required.message(), Some("Required")); + assert_eq!(required.as_str(), "60 Required\r\n"); + + let required = Required::from_utf8("60\r\n".as_bytes()).unwrap(); + assert_eq!(required.message(), None); + assert_eq!(required.as_str(), "60\r\n"); + + // err + assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/required/error.rs b/src/client/connection/response/certificate/required/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/required/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From 4eb998ef20767da11c039360158fb11030fee954 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 22:51:55 +0200 Subject: [PATCH 67/95] draft potential test --- src/client/connection/response/input/default.rs | 4 ++-- src/client/connection/response/input/sensitive.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index f0a04f3..8d24b62 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -54,8 +54,8 @@ fn test() { assert_eq!(default.as_str(), "10\r\n"); // err - assert!(Default::from_utf8("12 Fail\r\n".as_bytes()).is_err()); - assert!(Default::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + // @TODO assert!(Default::from_utf8("10Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail".as_bytes()).is_err()); } diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index d456490..cc6ab5d 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -54,8 +54,8 @@ fn test() { assert_eq!(sensitive.as_str(), "11\r\n"); // err - assert!(Sensitive::from_utf8("12 Fail\r\n".as_bytes()).is_err()); - assert!(Sensitive::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + // @TODO assert!(Sensitive::from_utf8("11Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); } From 473ed48715385fc88845d9081416e4e026eddf98 Mon Sep 17 00:00:00 2001 From: yggverse Date: Mon, 24 Mar 2025 23:32:18 +0200 Subject: [PATCH 68/95] make final codes public, add comments --- .../connection/response/certificate/not_authorized.rs | 5 +++-- src/client/connection/response/certificate/not_valid.rs | 5 +++-- src/client/connection/response/certificate/required.rs | 5 +++-- src/client/connection/response/input/default.rs | 5 +++-- src/client/connection/response/input/sensitive.rs | 5 +++-- src/client/connection/response/success/default.rs | 6 +++++- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs index fe85d1e..980d9ac 100644 --- a/src/client/connection/response/certificate/not_authorized.rs +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"61"; +/// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +pub const CODE: &[u8] = b"61"; -/// Hold header `String` for [61](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +/// Hold header `String` for [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct NotAuthorized(String); diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs index 35ad475..933b694 100644 --- a/src/client/connection/response/certificate/not_valid.rs +++ b/src/client/connection/response/certificate/not_valid.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"62"; +/// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +pub const CODE: &[u8] = b"62"; -/// Hold header `String` for [62](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +/// Hold header `String` for [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct NotValid(String); diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs index df0ef63..3d8f48d 100644 --- a/src/client/connection/response/certificate/required.rs +++ b/src/client/connection/response/certificate/required.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"60"; +/// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +pub const CODE: &[u8] = b"60"; -/// Hold header `String` for [60](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +/// Hold header `String` for [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct Required(String); diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index 8d24b62..93143f7 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"10"; +/// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +pub const CODE: &[u8] = b"10"; -/// Hold header `String` for [10](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +/// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct Default(String); diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index cc6ab5d..9219d39 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -1,9 +1,10 @@ pub mod error; pub use error::Error; -const CODE: &[u8] = b"11"; +/// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +pub const CODE: &[u8] = b"11"; -/// Hold header `String` for [11](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +/// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction pub struct Sensitive(String); diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index 6180f2e..a905318 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -4,8 +4,12 @@ pub mod header; pub use error::Error; pub use header::Header; -const CODE: &[u8] = b"20"; +/// [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code +pub const CODE: &[u8] = b"20"; +/// Holder for [Success](https://geminiprotocol.net/docs/protocol-specification.gmi#success) status code +/// * this response type MAY contain body data +/// * the header has closed members to require valid construction pub struct Default { pub header: Header, pub content: Option>, From 5229cdae858ebe210c28ab3b1eb19f46da62a90e Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 02:18:02 +0200 Subject: [PATCH 69/95] reorganize redirection structs format: make constructors lazy, parse members on get --- src/client/connection/response/redirect.rs | 346 ++++++++---------- .../connection/response/redirect/error.rs | 59 ++- .../connection/response/redirect/permanent.rs | 79 ++++ .../response/redirect/permanent/error.rs | 32 ++ .../connection/response/redirect/temporary.rs | 79 ++++ .../response/redirect/temporary/error.rs | 32 ++ 6 files changed, 414 insertions(+), 213 deletions(-) create mode 100644 src/client/connection/response/redirect/permanent.rs create mode 100644 src/client/connection/response/redirect/permanent/error.rs create mode 100644 src/client/connection/response/redirect/temporary.rs create mode 100644 src/client/connection/response/redirect/temporary/error.rs diff --git a/src/client/connection/response/redirect.rs b/src/client/connection/response/redirect.rs index 2554fdf..48bd610 100644 --- a/src/client/connection/response/redirect.rs +++ b/src/client/connection/response/redirect.rs @@ -1,16 +1,23 @@ pub mod error; -pub use error::Error; +pub mod permanent; +pub mod temporary; -use glib::{GStringPtr, Uri, UriFlags}; +pub use error::{Error, UriError}; +pub use permanent::Permanent; +pub use temporary::Temporary; -const TEMPORARY: (u8, &str) = (30, "Temporary redirect"); -const PERMANENT: (u8, &str) = (31, "Permanent redirect"); +// Local dependencies +use glib::{Uri, UriFlags}; + +const CODE: u8 = b'3'; + +/// [Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection) statuses pub enum Redirect { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection - Temporary { target: String }, + Temporary(Temporary), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection - Permanent { target: String }, + Permanent(Permanent), } impl Redirect { @@ -18,210 +25,161 @@ impl Redirect { /// 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)), - } - } - - // Convertors - - pub fn to_code(&self) -> u8 { - match self { - Self::Permanent { .. } => PERMANENT, - Self::Temporary { .. } => TEMPORARY, - } - .0 - } - - /// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), - /// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` - /// * fragment implementation uncompleted @TODO - pub fn to_uri(&self, base: &Uri) -> Result { - match Uri::build( - UriFlags::NONE, - base.scheme().as_str(), - None, // unexpected - base.host().as_deref(), - base.port(), - base.path().as_str(), - // > If a server sends a redirection in response to a request with a query string, - // > the client MUST NOT apply the query string to the new location - None, - // > A server SHOULD NOT include fragments in redirections, - // > but if one is given, and a client already has a fragment it could apply (from the original URI), - // > it is up to the client which fragment to apply. - None, // @TODO - ) - .parse_relative( - &{ - // URI started with double slash yet not supported by Glib function - // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 - let t = self.target(); - match t.strip_prefix("//") { - Some(p) => { - let postfix = p.trim_start_matches(":"); - format!( - "{}://{}", - base.scheme(), - if postfix.is_empty() { - match base.host() { - Some(h) => format!("{h}/"), - None => return Err(Error::BaseHost), - } - } else { - postfix.to_string() - } - ) - } - None => t.to_string(), - } + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Temporary( + Temporary::from_utf8(buffer).map_err(Error::Temporary)?, + )), + b'1' => Ok(Self::Permanent( + Permanent::from_utf8(buffer).map_err(Error::Permanent)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), }, - UriFlags::NONE, - ) { - Ok(absolute) => Ok(absolute), - Err(e) => Err(Error::Uri(e)), + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn target(&self) -> &str { + pub fn target(&self) -> Result<&str, Error> { match self { - Self::Permanent { target } => target, - Self::Temporary { target } => target, + Self::Temporary(temporary) => temporary.target().map_err(Error::Temporary), + Self::Permanent(permanent) => permanent.target().map_err(Error::Permanent), + } + } + + pub fn as_str(&self) -> &str { + match self { + Self::Temporary(temporary) => temporary.as_str(), + Self::Permanent(permanent) => permanent.as_str(), + } + } + + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Temporary(temporary) => temporary.as_bytes(), + Self::Permanent(permanent) => permanent.as_bytes(), + } + } + + pub fn uri(&self, base: &Uri) -> Result { + match self { + Self::Temporary(temporary) => temporary.uri(base).map_err(Error::Temporary), + Self::Permanent(permanent) => permanent.uri(base).map_err(Error::Permanent), } } } -impl std::fmt::Display for Redirect { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Permanent { .. } => PERMANENT, - Self::Temporary { .. } => TEMPORARY, +// Tools + +/// Resolve [specification-compatible](https://geminiprotocol.net/docs/protocol-specification.gmi#redirection), +/// absolute [Uri](https://docs.gtk.org/glib/struct.Uri.html) for `target` using `base` +/// * fragment implementation uncompleted @TODO +fn uri(target: &str, base: &Uri) -> Result { + match Uri::build( + UriFlags::NONE, + base.scheme().as_str(), + None, // unexpected + base.host().as_deref(), + base.port(), + base.path().as_str(), + // > If a server sends a redirection in response to a request with a query string, + // > the client MUST NOT apply the query string to the new location + None, + // > A server SHOULD NOT include fragments in redirections, + // > but if one is given, and a client already has a fragment it could apply (from the original URI), + // > it is up to the client which fragment to apply. + None, // @TODO + ) + .parse_relative( + &{ + // URI started with double slash yet not supported by Glib function + // https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + let t = target; + match t.strip_prefix("//") { + Some(p) => { + let postfix = p.trim_start_matches(":"); + format!( + "{}://{}", + base.scheme(), + if postfix.is_empty() { + match base.host() { + Some(h) => format!("{h}/"), + None => return Err(UriError::BaseHost), + } + } else { + postfix.to_string() + } + ) + } + None => t.to_string(), } - .1 - ) - } -} - -impl std::str::FromStr for Redirect { - type Err = Error; - fn from_str(header: &str) -> Result { - use glib::{Regex, RegexCompileFlags, RegexMatchFlags}; - - let regex = Regex::split_simple( - r"^3(\d)\s([^\r\n]+)", - header, - RegexCompileFlags::DEFAULT, - RegexMatchFlags::DEFAULT, - ); - - match regex.get(1) { - Some(code) => match code.as_str() { - "0" => Ok(Self::Temporary { - target: target(regex.get(2))?, - }), - "1" => Ok(Self::Permanent { - target: target(regex.get(2))?, - }), - _ => todo!(), - }, - None => Err(Error::Protocol), - } - } -} - -fn target(value: Option<&GStringPtr>) -> Result { - match value { - Some(target) => { - let target = target.trim(); - if target.is_empty() { - Err(Error::Target) - } else { - Ok(target.to_string()) - } - } - None => Err(Error::Target), + }, + UriFlags::NONE, + ) { + Ok(absolute) => Ok(absolute), + Err(e) => Err(UriError::ParseRelative(e)), } } #[test] fn test() { - use std::str::FromStr; - { - let temporary = Redirect::from_str("30 /uri\r\n").unwrap(); - assert_eq!(temporary.target(), "/uri"); - assert_eq!(temporary.to_code(), TEMPORARY.0); - assert_eq!(temporary.to_string(), TEMPORARY.1); - - let permanent = Redirect::from_str("31 /uri\r\n").unwrap(); - assert_eq!(permanent.target(), "/uri"); - assert_eq!(permanent.to_code(), PERMANENT.0); - assert_eq!(permanent.to_string(), PERMANENT.1); - } - { - let base = Uri::build( - UriFlags::NONE, - "gemini", - None, - Some("geminiprotocol.net"), - -1, - "/path/", - Some("query"), - Some("fragment"), - ); - assert_eq!( - Redirect::from_str("30 /uri\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/uri" - ); - assert_eq!( - Redirect::from_str("30 uri\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/path/uri" - ); - assert_eq!( - Redirect::from_str("30 gemini://test.host/uri\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://test.host/uri" - ); - assert_eq!( - Redirect::from_str("30 //\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/" - ); - assert_eq!( - Redirect::from_str("30 //geminiprotocol.net/path\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/path" - ); - assert_eq!( - Redirect::from_str("30 //:\r\n") - .unwrap() - .to_uri(&base) - .unwrap() - .to_string(), - "gemini://geminiprotocol.net/" - ); + /// Test common assertion rules + fn t(base: &Uri, source: &str, target: &str) { + let b = source.as_bytes(); + let r = Redirect::from_utf8(b).unwrap(); + assert!(r.uri(base).is_ok_and(|u| u.to_string() == target)); + assert_eq!(r.as_str(), source); + assert_eq!(r.as_bytes(), b); } + // common base + let base = Uri::build( + UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + // codes test + t( + &base, + "30 gemini://geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", + ); + t( + &base, + "31 gemini://geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", + ); + // relative test + t( + &base, + "31 path\r\n", + "gemini://geminiprotocol.net/path/path", + ); + t( + &base, + "31 //geminiprotocol.net\r\n", + "gemini://geminiprotocol.net", + ); + t( + &base, + "31 //geminiprotocol.net/path\r\n", + "gemini://geminiprotocol.net/path", + ); + t(&base, "31 /path\r\n", "gemini://geminiprotocol.net/path"); + t(&base, "31 //:\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 //\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 /\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 ../\r\n", "gemini://geminiprotocol.net/"); + t(&base, "31 ..\r\n", "gemini://geminiprotocol.net/"); } diff --git a/src/client/connection/response/redirect/error.rs b/src/client/connection/response/redirect/error.rs index eeaf2ed..38aaab1 100644 --- a/src/client/connection/response/redirect/error.rs +++ b/src/client/connection/response/redirect/error.rs @@ -1,34 +1,55 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - BaseHost, - Uri(glib::Error), - Protocol, - Target, - Utf8Error(Utf8Error), + FirstByte(u8), + Permanent(super::permanent::Error), + SecondByte(u8), + Temporary(super::temporary::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::BaseHost => { - write!(f, "Base host required") + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } - Self::Uri(e) => { - write!(f, "URI error: {e}") + Self::Permanent(e) => { + write!(f, "Permanent parse error: {e}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") } - Self::Protocol => { - write!(f, "Protocol error") + Self::Temporary(e) => { + write!(f, "Temporary parse error: {e}") } - Self::Target => { - write!(f, "Target error") + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") + } + } + } +} + +/// Handle `super::uri` method +#[derive(Debug)] +pub enum UriError { + BaseHost, + ParseRelative(glib::Error), +} + +impl Display for UriError { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::BaseHost => { + write!(f, "URI base host required") + } + Self::ParseRelative(e) => { + write!(f, "URI parse relative error: {e}") } } } diff --git a/src/client/connection/response/redirect/permanent.rs b/src/client/connection/response/redirect/permanent.rs new file mode 100644 index 0000000..843e929 --- /dev/null +++ b/src/client/connection/response/redirect/permanent.rs @@ -0,0 +1,79 @@ +pub mod error; +pub use error::Error; + +// Local dependencies + +use glib::Uri; + +/// [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code +pub const CODE: &[u8] = b"31"; + +/// Hold header `String` for [Permanent Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-31-permanent-redirection) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Permanent(String); + +impl Permanent { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get raw target for `Self` + /// * return `Err` if the required target is empty + pub fn target(&self) -> Result<&str, Error> { + self.0 + .get(2..) + .map(|s| s.trim()) + .filter(|x| !x.is_empty()) + .ok_or(Error::TargetEmpty) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn uri(&self, base: &Uri) -> Result { + super::uri(self.target()?, base).map_err(Error::Uri) + } +} + +#[test] +fn test() { + const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n"; + let base = Uri::build( + glib::UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + let permanent = Permanent::from_utf8(BUFFER.as_bytes()).unwrap(); + assert!(permanent.target().is_ok()); + assert!( + permanent + .uri(&base) + .is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path") + ); + assert!(Permanent::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/redirect/permanent/error.rs b/src/client/connection/response/redirect/permanent/error.rs new file mode 100644 index 0000000..0d0e81d --- /dev/null +++ b/src/client/connection/response/redirect/permanent/error.rs @@ -0,0 +1,32 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + TargetEmpty, + Uri(super::super::UriError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::TargetEmpty => { + write!(f, "Expected target is empty") + } + Self::Uri(e) => { + write!(f, "URI parse error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/redirect/temporary.rs b/src/client/connection/response/redirect/temporary.rs new file mode 100644 index 0000000..df41731 --- /dev/null +++ b/src/client/connection/response/redirect/temporary.rs @@ -0,0 +1,79 @@ +pub mod error; +pub use error::Error; + +// Local dependencies + +use glib::Uri; + +/// [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code +pub const CODE: &[u8] = b"30"; + +/// Hold header `String` for [Temporary Redirection](https://geminiprotocol.net/docs/protocol-specification.gmi#status-30-temporary-redirection) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Temporary(String); + +impl Temporary { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get raw target for `Self` + /// * return `Err` if the required target is empty + pub fn target(&self) -> Result<&str, Error> { + self.0 + .get(2..) + .map(|s| s.trim()) + .filter(|x| !x.is_empty()) + .ok_or(Error::TargetEmpty) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + pub fn uri(&self, base: &Uri) -> Result { + super::uri(self.target()?, base).map_err(Error::Uri) + } +} + +#[test] +fn test() { + const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n"; + let base = Uri::build( + glib::UriFlags::NONE, + "gemini", + None, + Some("geminiprotocol.net"), + -1, + "/path/", + Some("query"), + Some("fragment"), + ); + let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap(); + assert!(temporary.target().is_ok()); + assert!( + temporary + .uri(&base) + .is_ok_and(|u| u.to_string() == "gemini://geminiprotocol.net/path") + ); + assert!(Temporary::from_utf8("32 gemini://geminiprotocol.net/path\r\n".as_bytes()).is_err()) +} diff --git a/src/client/connection/response/redirect/temporary/error.rs b/src/client/connection/response/redirect/temporary/error.rs new file mode 100644 index 0000000..0d0e81d --- /dev/null +++ b/src/client/connection/response/redirect/temporary/error.rs @@ -0,0 +1,32 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + TargetEmpty, + Uri(super::super::UriError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::TargetEmpty => { + write!(f, "Expected target is empty") + } + Self::Uri(e) => { + write!(f, "URI parse error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From f513747e862ae318532f942f8860e1e85f0a3485 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 02:21:03 +0200 Subject: [PATCH 70/95] add alias getters test --- src/client/connection/response/redirect/permanent.rs | 5 ++++- src/client/connection/response/redirect/temporary.rs | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/connection/response/redirect/permanent.rs b/src/client/connection/response/redirect/permanent.rs index 843e929..e8e6371 100644 --- a/src/client/connection/response/redirect/permanent.rs +++ b/src/client/connection/response/redirect/permanent.rs @@ -58,6 +58,7 @@ impl Permanent { #[test] fn test() { const BUFFER: &str = "31 gemini://geminiprotocol.net/path\r\n"; + let bytes = BUFFER.as_bytes(); let base = Uri::build( glib::UriFlags::NONE, "gemini", @@ -68,7 +69,9 @@ fn test() { Some("query"), Some("fragment"), ); - let permanent = Permanent::from_utf8(BUFFER.as_bytes()).unwrap(); + let permanent = Permanent::from_utf8(bytes).unwrap(); + assert_eq!(permanent.as_str(), BUFFER); + assert_eq!(permanent.as_bytes(), bytes); assert!(permanent.target().is_ok()); assert!( permanent diff --git a/src/client/connection/response/redirect/temporary.rs b/src/client/connection/response/redirect/temporary.rs index df41731..a131336 100644 --- a/src/client/connection/response/redirect/temporary.rs +++ b/src/client/connection/response/redirect/temporary.rs @@ -58,6 +58,7 @@ impl Temporary { #[test] fn test() { const BUFFER: &str = "30 gemini://geminiprotocol.net/path\r\n"; + let bytes = BUFFER.as_bytes(); let base = Uri::build( glib::UriFlags::NONE, "gemini", @@ -69,6 +70,8 @@ fn test() { Some("fragment"), ); let temporary = Temporary::from_utf8(BUFFER.as_bytes()).unwrap(); + assert_eq!(temporary.as_str(), BUFFER); + assert_eq!(temporary.as_bytes(), bytes); assert!(temporary.target().is_ok()); assert!( temporary From 3b24625d66f45c3fb763759424f5482f869ef308 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:01:19 +0200 Subject: [PATCH 71/95] implement `message_or_default` method, add comments --- src/client/connection/response/certificate.rs | 14 ++++++++++++++ .../response/certificate/not_authorized.rs | 15 ++++++++++++++- .../connection/response/certificate/not_valid.rs | 15 ++++++++++++++- .../connection/response/certificate/required.rs | 15 ++++++++++++++- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index e67ded4..07e2891 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -51,6 +51,8 @@ impl Certificate { // Getters + /// Get optional message for `Self` + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { match self { Self::Required(required) => required.message(), @@ -59,6 +61,17 @@ impl Certificate { } } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Required(required) => required.message_or_default(), + Self::NotAuthorized(not_authorized) => not_authorized.message_or_default(), + Self::NotValid(not_valid) => not_valid.message_or_default(), + } + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { match self { Self::Required(required) => required.as_str(), @@ -67,6 +80,7 @@ impl Certificate { } } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { match self { Self::Required(required) => required.as_bytes(), diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs index 980d9ac..0c73ae7 100644 --- a/src/client/connection/response/certificate/not_authorized.rs +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code pub const CODE: &[u8] = b"61"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Certificate is not authorized"; + /// Hold header `String` for [Not Authorized](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -29,15 +34,23 @@ impl NotAuthorized { // Getters /// Get optional message for `Self` - /// * return `None` if the message is empty + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs index 933b694..58198a8 100644 --- a/src/client/connection/response/certificate/not_valid.rs +++ b/src/client/connection/response/certificate/not_valid.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code pub const CODE: &[u8] = b"62"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Certificate is not valid"; + /// Hold header `String` for [Not Valid](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -29,15 +34,23 @@ impl NotValid { // Getters /// Get optional message for `Self` - /// * return `None` if the message is empty + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs index 3d8f48d..105185a 100644 --- a/src/client/connection/response/certificate/required.rs +++ b/src/client/connection/response/certificate/required.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code pub const CODE: &[u8] = b"60"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Certificate required"; + /// Hold header `String` for [Certificate Required](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -29,15 +34,23 @@ impl Required { // Getters /// Get optional message for `Self` - /// * return `None` if the message is empty + /// * return `None` if the message is empty (not provided by server) pub fn message(&self) -> Option<&str> { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } From d565d56c1728cec3794b570774f986b2bb2eeb77 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:20:40 +0200 Subject: [PATCH 72/95] implement `message_or_default` method, add comments, add missed members test --- src/client/connection/response/input.rs | 11 +++++++++++ .../connection/response/input/default.rs | 18 +++++++++++++++++- .../connection/response/input/sensitive.rs | 18 +++++++++++++++++- 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index 549b13e..0cd9857 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -48,6 +48,16 @@ impl Input { } } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::Sensitive(sensitive) => sensitive.message_or_default(), + } + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { match self { Self::Default(default) => default.as_str(), @@ -55,6 +65,7 @@ impl Input { } } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { match self { Self::Default(default) => default.as_bytes(), diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index 93143f7..9b9f096 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code pub const CODE: &[u8] = b"10"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Input expected"; + /// Hold header `String` for [Input Expected](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -34,10 +39,18 @@ impl Default { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } @@ -48,14 +61,17 @@ fn test() { // ok let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); assert_eq!(default.message(), Some("Default")); + assert_eq!(default.message_or_default(), "Default"); assert_eq!(default.as_str(), "10 Default\r\n"); + assert_eq!(default.as_bytes(), "10 Default\r\n".as_bytes()); let default = Default::from_utf8("10\r\n".as_bytes()).unwrap(); assert_eq!(default.message(), None); + assert_eq!(default.message_or_default(), DEFAULT_MESSAGE); assert_eq!(default.as_str(), "10\r\n"); + assert_eq!(default.as_bytes(), "10\r\n".as_bytes()); // err - // @TODO assert!(Default::from_utf8("10Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Default::from_utf8("Fail".as_bytes()).is_err()); diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index 9219d39..4646776 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -4,6 +4,11 @@ pub use error::Error; /// [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code pub const CODE: &[u8] = b"11"; +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Sensitive input expected"; + /// Hold header `String` for [Sensitive Input](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code /// * this response type does not contain body data /// * the header member is closed to require valid construction @@ -34,10 +39,18 @@ impl Sensitive { self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) } + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` pub fn as_str(&self) -> &str { &self.0 } + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { self.0.as_bytes() } @@ -48,14 +61,17 @@ fn test() { // ok let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); assert_eq!(sensitive.message(), Some("Sensitive")); + assert_eq!(sensitive.message_or_default(), "Sensitive"); assert_eq!(sensitive.as_str(), "11 Sensitive\r\n"); + assert_eq!(sensitive.as_bytes(), "11 Sensitive\r\n".as_bytes()); let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.message_or_default(), DEFAULT_MESSAGE); assert_eq!(sensitive.as_str(), "11\r\n"); + assert_eq!(sensitive.as_bytes(), "11\r\n".as_bytes()); // err - // @TODO assert!(Sensitive::from_utf8("11Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); From 064c4107f33174dfe872f861273415c8bd3d4fb9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:21:59 +0200 Subject: [PATCH 73/95] reduce local var names len --- .../connection/response/input/default.rs | 20 +++++++++---------- .../connection/response/input/sensitive.rs | 20 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs index 9b9f096..4a5a3df 100644 --- a/src/client/connection/response/input/default.rs +++ b/src/client/connection/response/input/default.rs @@ -59,17 +59,17 @@ impl Default { #[test] fn test() { // ok - let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); - assert_eq!(default.message(), Some("Default")); - assert_eq!(default.message_or_default(), "Default"); - assert_eq!(default.as_str(), "10 Default\r\n"); - assert_eq!(default.as_bytes(), "10 Default\r\n".as_bytes()); + let d = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Default")); + assert_eq!(d.message_or_default(), "Default"); + assert_eq!(d.as_str(), "10 Default\r\n"); + assert_eq!(d.as_bytes(), "10 Default\r\n".as_bytes()); - let default = Default::from_utf8("10\r\n".as_bytes()).unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.message_or_default(), DEFAULT_MESSAGE); - assert_eq!(default.as_str(), "10\r\n"); - assert_eq!(default.as_bytes(), "10\r\n".as_bytes()); + let d = Default::from_utf8("10\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "10\r\n"); + assert_eq!(d.as_bytes(), "10\r\n".as_bytes()); // err assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs index 4646776..594c8fb 100644 --- a/src/client/connection/response/input/sensitive.rs +++ b/src/client/connection/response/input/sensitive.rs @@ -59,17 +59,17 @@ impl Sensitive { #[test] fn test() { // ok - let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); - assert_eq!(sensitive.message(), Some("Sensitive")); - assert_eq!(sensitive.message_or_default(), "Sensitive"); - assert_eq!(sensitive.as_str(), "11 Sensitive\r\n"); - assert_eq!(sensitive.as_bytes(), "11 Sensitive\r\n".as_bytes()); + let s = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); + assert_eq!(s.message(), Some("Sensitive")); + assert_eq!(s.message_or_default(), "Sensitive"); + assert_eq!(s.as_str(), "11 Sensitive\r\n"); + assert_eq!(s.as_bytes(), "11 Sensitive\r\n".as_bytes()); - let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); - assert_eq!(sensitive.message(), None); - assert_eq!(sensitive.message_or_default(), DEFAULT_MESSAGE); - assert_eq!(sensitive.as_str(), "11\r\n"); - assert_eq!(sensitive.as_bytes(), "11\r\n".as_bytes()); + let s = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); + assert_eq!(s.message(), None); + assert_eq!(s.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(s.as_str(), "11\r\n"); + assert_eq!(s.as_bytes(), "11\r\n".as_bytes()); // err assert!(Sensitive::from_utf8("13 Fail\r\n".as_bytes()).is_err()); From 0c75da793ff9d4dde0a343ee680e79b29bf77a72 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 03:26:39 +0200 Subject: [PATCH 74/95] add missed tests members, enshort local var names --- .../response/certificate/not_authorized.rs | 16 ++++++++++------ .../connection/response/certificate/not_valid.rs | 16 ++++++++++------ .../connection/response/certificate/required.rs | 16 ++++++++++------ 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs index 0c73ae7..b10d6ff 100644 --- a/src/client/connection/response/certificate/not_authorized.rs +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -59,13 +59,17 @@ impl NotAuthorized { #[test] fn test() { // ok - let not_authorized = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); - assert_eq!(not_authorized.message(), Some("Not Authorized")); - assert_eq!(not_authorized.as_str(), "61 Not Authorized\r\n"); + let na = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); + assert_eq!(na.message(), Some("Not Authorized")); + assert_eq!(na.message_or_default(), "Not Authorized"); + assert_eq!(na.as_str(), "61 Not Authorized\r\n"); + assert_eq!(na.as_bytes(), "61 Not Authorized\r\n".as_bytes()); - let not_authorized = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); - assert_eq!(not_authorized.message(), None); - assert_eq!(not_authorized.as_str(), "61\r\n"); + let na = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); + assert_eq!(na.message(), None); + assert_eq!(na.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(na.as_str(), "61\r\n"); + assert_eq!(na.as_bytes(), "61\r\n".as_bytes()); // err assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); diff --git a/src/client/connection/response/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs index 58198a8..94e847c 100644 --- a/src/client/connection/response/certificate/not_valid.rs +++ b/src/client/connection/response/certificate/not_valid.rs @@ -59,13 +59,17 @@ impl NotValid { #[test] fn test() { // ok - let not_valid = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); - assert_eq!(not_valid.message(), Some("Not Valid")); - assert_eq!(not_valid.as_str(), "62 Not Valid\r\n"); + let nv = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); + assert_eq!(nv.message(), Some("Not Valid")); + assert_eq!(nv.message_or_default(), "Not Valid"); + assert_eq!(nv.as_str(), "62 Not Valid\r\n"); + assert_eq!(nv.as_bytes(), "62 Not Valid\r\n".as_bytes()); - let not_valid = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); - assert_eq!(not_valid.message(), None); - assert_eq!(not_valid.as_str(), "62\r\n"); + let nv = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); + assert_eq!(nv.message(), None); + assert_eq!(nv.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(nv.as_str(), "62\r\n"); + assert_eq!(nv.as_bytes(), "62\r\n".as_bytes()); // err // @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err()); diff --git a/src/client/connection/response/certificate/required.rs b/src/client/connection/response/certificate/required.rs index 105185a..b44585c 100644 --- a/src/client/connection/response/certificate/required.rs +++ b/src/client/connection/response/certificate/required.rs @@ -59,13 +59,17 @@ impl Required { #[test] fn test() { // ok - let required = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); - assert_eq!(required.message(), Some("Required")); - assert_eq!(required.as_str(), "60 Required\r\n"); + let r = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); + assert_eq!(r.message(), Some("Required")); + assert_eq!(r.message_or_default(), "Required"); + assert_eq!(r.as_str(), "60 Required\r\n"); + assert_eq!(r.as_bytes(), "60 Required\r\n".as_bytes()); - let required = Required::from_utf8("60\r\n".as_bytes()).unwrap(); - assert_eq!(required.message(), None); - assert_eq!(required.as_str(), "60\r\n"); + let r = Required::from_utf8("60\r\n".as_bytes()).unwrap(); + assert_eq!(r.message(), None); + assert_eq!(r.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(r.as_str(), "60\r\n"); + assert_eq!(r.as_bytes(), "60\r\n".as_bytes()); // err assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); From e96ff688b364d3bcf276c1edeb2760677b2555bc Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 04:43:56 +0200 Subject: [PATCH 75/95] update permanent status codes api --- src/client/connection/response/failure.rs | 11 +- .../connection/response/failure/error.rs | 6 +- .../connection/response/failure/permanent.rs | 230 +++++++----------- .../response/failure/permanent/bad_request.rs | 78 ++++++ .../failure/permanent/bad_request/error.rs | 24 ++ .../response/failure/permanent/default.rs | 78 ++++++ .../failure/permanent/default/error.rs | 24 ++ .../response/failure/permanent/error.rs | 45 +++- .../response/failure/permanent/gone.rs | 78 ++++++ .../response/failure/permanent/gone/error.rs | 24 ++ .../response/failure/permanent/not_found.rs | 78 ++++++ .../failure/permanent/not_found/error.rs | 24 ++ .../permanent/proxy_request_refused.rs | 78 ++++++ .../permanent/proxy_request_refused/error.rs | 24 ++ 14 files changed, 644 insertions(+), 158 deletions(-) create mode 100644 src/client/connection/response/failure/permanent/bad_request.rs create mode 100644 src/client/connection/response/failure/permanent/bad_request/error.rs create mode 100644 src/client/connection/response/failure/permanent/default.rs create mode 100644 src/client/connection/response/failure/permanent/default/error.rs create mode 100644 src/client/connection/response/failure/permanent/gone.rs create mode 100644 src/client/connection/response/failure/permanent/gone/error.rs create mode 100644 src/client/connection/response/failure/permanent/not_found.rs create mode 100644 src/client/connection/response/failure/permanent/not_found/error.rs create mode 100644 src/client/connection/response/failure/permanent/proxy_request_refused.rs create mode 100644 src/client/connection/response/failure/permanent/proxy_request_refused/error.rs diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 40c8abf..419af23 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -21,7 +21,7 @@ impl Failure { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { match buffer.first() { - Some(byte) => match byte { + Some(b) => match b { b'4' => match Temporary::from_utf8(buffer) { Ok(input) => Ok(Self::Temporary(input)), Err(e) => Err(Error::Temporary(e)), @@ -30,7 +30,7 @@ impl Failure { Ok(failure) => Ok(Self::Permanent(failure)), Err(e) => Err(Error::Permanent(e)), }, - _ => Err(Error::Code), + b => Err(Error::Code(*b)), }, None => Err(Error::Protocol), } @@ -38,13 +38,6 @@ impl Failure { // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Permanent(permanent) => permanent.to_code(), - Self::Temporary(temporary) => temporary.to_code(), - } - } - pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), diff --git a/src/client/connection/response/failure/error.rs b/src/client/connection/response/failure/error.rs index 7725b92..056f714 100644 --- a/src/client/connection/response/failure/error.rs +++ b/src/client/connection/response/failure/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, + Code(u8), Permanent(super::permanent::Error), Protocol, Temporary(super::temporary::Error), @@ -11,8 +11,8 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Code group error") + Self::Code(b) => { + write!(f, "Unexpected status code byte: {b}") } Self::Permanent(e) => { write!(f, "Permanent failure group error: {e}") diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index e2ab9e0..ffe4ea3 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -1,24 +1,31 @@ +pub mod bad_request; +pub mod default; pub mod error; -pub use error::Error; +pub mod gone; +pub mod not_found; +pub mod proxy_request_refused; -const DEFAULT: (u8, &str) = (50, "Unspecified"); -const NOT_FOUND: (u8, &str) = (51, "Not found"); -const GONE: (u8, &str) = (52, "Gone"); -const PROXY_REQUEST_REFUSED: (u8, &str) = (53, "Proxy request refused"); -const BAD_REQUEST: (u8, &str) = (59, "bad-request"); +pub use bad_request::BadRequest; +pub use default::Default; +pub use error::Error; +pub use gone::Gone; +pub use not_found::NotFound; +pub use proxy_request_refused::ProxyRequestRefused; + +const CODE: u8 = b'5'; /// https://geminiprotocol.net/docs/protocol-specification.gmi#permanent-failure pub enum Permanent { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-50 - Default { message: Option }, + Default(Default), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found - NotFound { message: Option }, + NotFound(NotFound), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone - Gone { message: Option }, + Gone(Gone), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused - ProxyRequestRefused { message: Option }, + ProxyRequestRefused(ProxyRequestRefused), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request - BadRequest { message: Option }, + BadRequest(BadRequest), } impl Permanent { @@ -26,154 +33,105 @@ impl Permanent { /// 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)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::NotFound( + NotFound::from_utf8(buffer).map_err(Error::NotFound)?, + )), + b'2' => Ok(Self::Gone(Gone::from_utf8(buffer).map_err(Error::Gone)?)), + b'3' => Ok(Self::ProxyRequestRefused( + ProxyRequestRefused::from_utf8(buffer) + .map_err(Error::ProxyRequestRefused)?, + )), + b'9' => Ok(Self::BadRequest( + BadRequest::from_utf8(buffer).map_err(Error::BadRequest)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Default { .. } => DEFAULT, - Self::NotFound { .. } => NOT_FOUND, - Self::Gone { .. } => GONE, - Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, - Self::BadRequest { .. } => BAD_REQUEST, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::NotFound { message } => message, - Self::Gone { message } => message, - Self::ProxyRequestRefused { message } => message, - Self::BadRequest { message } => message, + Self::Default(default) => default.message(), + Self::NotFound(not_found) => not_found.message(), + Self::Gone(gone) => gone.message(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.message(), + Self::BadRequest(bad_request) => bad_request.message(), } - .as_deref() } -} -impl std::fmt::Display for Permanent { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Default { .. } => DEFAULT, - Self::NotFound { .. } => NOT_FOUND, - Self::Gone { .. } => GONE, - Self::ProxyRequestRefused { .. } => PROXY_REQUEST_REFUSED, - Self::BadRequest { .. } => BAD_REQUEST, + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::NotFound(not_found) => not_found.message_or_default(), + Self::Gone(gone) => gone.message_or_default(), + Self::ProxyRequestRefused(proxy_request_refused) => { + proxy_request_refused.message_or_default() } - .1 - ) + Self::BadRequest(bad_request) => bad_request.message_or_default(), + } } -} -impl std::str::FromStr for Permanent { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("50") { - return Ok(Self::Default { - message: message(postfix), - }); + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::NotFound(not_found) => not_found.as_str(), + Self::Gone(gone) => gone.as_str(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_str(), + Self::BadRequest(bad_request) => bad_request.as_str(), } - if let Some(postfix) = header.strip_prefix("51") { - return Ok(Self::NotFound { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("52") { - return Ok(Self::Gone { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("53") { - return Ok(Self::ProxyRequestRefused { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("59") { - return Ok(Self::BadRequest { - message: message(postfix), - }); - } - Err(Error::Code) } -} -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::NotFound(not_found) => not_found.as_bytes(), + Self::Gone(gone) => gone.as_bytes(), + Self::ProxyRequestRefused(proxy_request_refused) => proxy_request_refused.as_bytes(), + Self::BadRequest(bad_request) => bad_request.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - +fn test() { + fn t(source: &str, message: Option<&str>) { + let b = source.as_bytes(); + let i = Permanent::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } // 50 - let default = Permanent::from_str("50 Message\r\n").unwrap(); - assert_eq!(default.message(), Some("Message")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Permanent::from_str("50\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - + t("50 Message\r\n", Some("Message")); + t("50\r\n", None); // 51 - let not_found = Permanent::from_str("51 Message\r\n").unwrap(); - assert_eq!(not_found.message(), Some("Message")); - assert_eq!(not_found.to_code(), NOT_FOUND.0); - assert_eq!(not_found.to_string(), NOT_FOUND.1); - - let not_found = Permanent::from_str("51\r\n").unwrap(); - assert_eq!(not_found.message(), None); - assert_eq!(not_found.to_code(), NOT_FOUND.0); - assert_eq!(not_found.to_string(), NOT_FOUND.1); - + t("51 Message\r\n", Some("Message")); + t("51\r\n", None); // 52 - let gone = Permanent::from_str("52 Message\r\n").unwrap(); - assert_eq!(gone.message(), Some("Message")); - assert_eq!(gone.to_code(), GONE.0); - assert_eq!(gone.to_string(), GONE.1); - - let gone = Permanent::from_str("52\r\n").unwrap(); - assert_eq!(gone.message(), None); - assert_eq!(gone.to_code(), GONE.0); - assert_eq!(gone.to_string(), GONE.1); - + t("52 Message\r\n", Some("Message")); + t("52\r\n", None); // 53 - let proxy_request_refused = Permanent::from_str("53 Message\r\n").unwrap(); - assert_eq!(proxy_request_refused.message(), Some("Message")); - assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); - assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); - - let proxy_request_refused = Permanent::from_str("53\r\n").unwrap(); - assert_eq!(proxy_request_refused.message(), None); - assert_eq!(proxy_request_refused.to_code(), PROXY_REQUEST_REFUSED.0); - assert_eq!(proxy_request_refused.to_string(), PROXY_REQUEST_REFUSED.1); - + t("53 Message\r\n", Some("Message")); + t("53\r\n", None); // 59 - let bad_request = Permanent::from_str("59 Message\r\n").unwrap(); - assert_eq!(bad_request.message(), Some("Message")); - assert_eq!(bad_request.to_code(), BAD_REQUEST.0); - assert_eq!(bad_request.to_string(), BAD_REQUEST.1); - - let bad_request = Permanent::from_str("59\r\n").unwrap(); - assert_eq!(bad_request.message(), None); - assert_eq!(bad_request.to_code(), BAD_REQUEST.0); - assert_eq!(bad_request.to_string(), BAD_REQUEST.1); + t("59 Message\r\n", Some("Message")); + t("59\r\n", None); } diff --git a/src/client/connection/response/failure/permanent/bad_request.rs b/src/client/connection/response/failure/permanent/bad_request.rs new file mode 100644 index 0000000..8cfa6f9 --- /dev/null +++ b/src/client/connection/response/failure/permanent/bad_request.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code +pub const CODE: &[u8] = b"59"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Bad request"; + +/// Hold header `String` for [Bad Request](https://geminiprotocol.net/docs/protocol-specification.gmi#status-59-bad-request) error status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct BadRequest(String); + +impl BadRequest { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let br = BadRequest::from_utf8("59 Message\r\n".as_bytes()).unwrap(); + assert_eq!(br.message(), Some("Message")); + assert_eq!(br.message_or_default(), "Message"); + assert_eq!(br.as_str(), "59 Message\r\n"); + assert_eq!(br.as_bytes(), "59 Message\r\n".as_bytes()); + + let br = BadRequest::from_utf8("59\r\n".as_bytes()).unwrap(); + assert_eq!(br.message(), None); + assert_eq!(br.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(br.as_str(), "59\r\n"); + assert_eq!(br.as_bytes(), "59\r\n".as_bytes()); + + // err + assert!(BadRequest::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(BadRequest::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(BadRequest::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/bad_request/error.rs b/src/client/connection/response/failure/permanent/bad_request/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/bad_request/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/default.rs b/src/client/connection/response/failure/permanent/default.rs new file mode 100644 index 0000000..466333d --- /dev/null +++ b/src/client/connection/response/failure/permanent/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code +pub const CODE: &[u8] = b"50"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Permanent error"; + +/// Hold header `String` for [Unspecified Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-50) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let d = Default::from_utf8("50 Message\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Message")); + assert_eq!(d.message_or_default(), "Message"); + assert_eq!(d.as_str(), "50 Message\r\n"); + assert_eq!(d.as_bytes(), "50 Message\r\n".as_bytes()); + + let d = Default::from_utf8("50\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "50\r\n"); + assert_eq!(d.as_bytes(), "50\r\n".as_bytes()); + + // err + assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/default/error.rs b/src/client/connection/response/failure/permanent/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/error.rs b/src/client/connection/response/failure/permanent/error.rs index 5cf1cf6..df334f5 100644 --- a/src/client/connection/response/failure/permanent/error.rs +++ b/src/client/connection/response/failure/permanent/error.rs @@ -1,22 +1,47 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + BadRequest(super::bad_request::Error), + Default(super::default::Error), + FirstByte(u8), + Gone(super::gone::Error), + NotFound(super::not_found::Error), + ProxyRequestRefused(super::proxy_request_refused::Error), + SecondByte(u8), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::BadRequest(e) => { + write!(f, "BadRequest parse error: {e}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") + } + Self::Gone(e) => { + write!(f, "Gone parse error: {e}") + } + Self::NotFound(e) => { + write!(f, "NotFound parse error: {e}") + } + Self::ProxyRequestRefused(e) => { + write!(f, "ProxyRequestRefused parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/failure/permanent/gone.rs b/src/client/connection/response/failure/permanent/gone.rs new file mode 100644 index 0000000..f93d068 --- /dev/null +++ b/src/client/connection/response/failure/permanent/gone.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code +pub const CODE: &[u8] = b"52"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Resource gone"; + +/// Hold header `String` for [Server Gone Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-52-gone) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Gone(String); + +impl Gone { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let g = Gone::from_utf8("52 Message\r\n".as_bytes()).unwrap(); + assert_eq!(g.message(), Some("Message")); + assert_eq!(g.message_or_default(), "Message"); + assert_eq!(g.as_str(), "52 Message\r\n"); + assert_eq!(g.as_bytes(), "52 Message\r\n".as_bytes()); + + let g = Gone::from_utf8("52\r\n".as_bytes()).unwrap(); + assert_eq!(g.message(), None); + assert_eq!(g.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(g.as_str(), "52\r\n"); + assert_eq!(g.as_bytes(), "52\r\n".as_bytes()); + + // err + assert!(Gone::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Gone::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Gone::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/gone/error.rs b/src/client/connection/response/failure/permanent/gone/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/gone/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/not_found.rs b/src/client/connection/response/failure/permanent/not_found.rs new file mode 100644 index 0000000..d5ddca9 --- /dev/null +++ b/src/client/connection/response/failure/permanent/not_found.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code +pub const CODE: &[u8] = b"51"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Not Found"; + +/// Hold header `String` for [Not Found Permanent Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-51-not-found) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotFound(String); + +impl NotFound { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let nf = NotFound::from_utf8("51 Message\r\n".as_bytes()).unwrap(); + assert_eq!(nf.message(), Some("Message")); + assert_eq!(nf.message_or_default(), "Message"); + assert_eq!(nf.as_str(), "51 Message\r\n"); + assert_eq!(nf.as_bytes(), "51 Message\r\n".as_bytes()); + + let nf = NotFound::from_utf8("51\r\n".as_bytes()).unwrap(); + assert_eq!(nf.message(), None); + assert_eq!(nf.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(nf.as_str(), "51\r\n"); + assert_eq!(nf.as_bytes(), "51\r\n".as_bytes()); + + // err + assert!(NotFound::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(NotFound::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotFound::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/not_found/error.rs b/src/client/connection/response/failure/permanent/not_found/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/not_found/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/permanent/proxy_request_refused.rs b/src/client/connection/response/failure/permanent/proxy_request_refused.rs new file mode 100644 index 0000000..fba229c --- /dev/null +++ b/src/client/connection/response/failure/permanent/proxy_request_refused.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code +pub const CODE: &[u8] = b"53"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Proxy request refused"; + +/// Hold header `String` for [Proxy Request Refused](https://geminiprotocol.net/docs/protocol-specification.gmi#status-53-proxy-request-refused) permanent error status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ProxyRequestRefused(String); + +impl ProxyRequestRefused { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let prf = ProxyRequestRefused::from_utf8("53 Message\r\n".as_bytes()).unwrap(); + assert_eq!(prf.message(), Some("Message")); + assert_eq!(prf.message_or_default(), "Message"); + assert_eq!(prf.as_str(), "53 Message\r\n"); + assert_eq!(prf.as_bytes(), "53 Message\r\n".as_bytes()); + + let prf = ProxyRequestRefused::from_utf8("53\r\n".as_bytes()).unwrap(); + assert_eq!(prf.message(), None); + assert_eq!(prf.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(prf.as_str(), "53\r\n"); + assert_eq!(prf.as_bytes(), "53\r\n".as_bytes()); + + // err + assert!(ProxyRequestRefused::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ProxyRequestRefused::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ProxyRequestRefused::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs b/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/permanent/proxy_request_refused/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From c9a59e76eeafcdbbc58274b4b4c859bd99b9f9a8 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 05:13:40 +0200 Subject: [PATCH 76/95] optimize tests format --- .../connection/response/failure/permanent.rs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/client/connection/response/failure/permanent.rs b/src/client/connection/response/failure/permanent.rs index ffe4ea3..526a208 100644 --- a/src/client/connection/response/failure/permanent.rs +++ b/src/client/connection/response/failure/permanent.rs @@ -112,26 +112,15 @@ impl Permanent { #[test] fn test() { - fn t(source: &str, message: Option<&str>) { + fn t(source: String, message: Option<&str>) { let b = source.as_bytes(); let i = Permanent::from_utf8(b).unwrap(); assert_eq!(i.message(), message); assert_eq!(i.as_str(), source); assert_eq!(i.as_bytes(), b); } - // 50 - t("50 Message\r\n", Some("Message")); - t("50\r\n", None); - // 51 - t("51 Message\r\n", Some("Message")); - t("51\r\n", None); - // 52 - t("52 Message\r\n", Some("Message")); - t("52\r\n", None); - // 53 - t("53 Message\r\n", Some("Message")); - t("53\r\n", None); - // 59 - t("59 Message\r\n", Some("Message")); - t("59\r\n", None); + for code in [50, 51, 52, 53, 59] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } From ea1fb8ea66687219e68dd8d22ccb0abca3143e50 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 05:13:56 +0200 Subject: [PATCH 77/95] update temporary status codes api --- src/client/connection/response/failure.rs | 42 ++++ .../connection/response/failure/temporary.rs | 231 +++++++----------- .../response/failure/temporary/cgi_error.rs | 78 ++++++ .../failure/temporary/cgi_error/error.rs | 24 ++ .../response/failure/temporary/default.rs | 78 ++++++ .../failure/temporary/default/error.rs | 24 ++ .../response/failure/temporary/error.rs | 45 +++- .../response/failure/temporary/proxy_error.rs | 78 ++++++ .../failure/temporary/proxy_error/error.rs | 24 ++ .../failure/temporary/server_unavailable.rs | 81 ++++++ .../temporary/server_unavailable/error.rs | 24 ++ .../response/failure/temporary/slow_down.rs | 81 ++++++ .../failure/temporary/slow_down/error.rs | 24 ++ 13 files changed, 682 insertions(+), 152 deletions(-) create mode 100644 src/client/connection/response/failure/temporary/cgi_error.rs create mode 100644 src/client/connection/response/failure/temporary/cgi_error/error.rs create mode 100644 src/client/connection/response/failure/temporary/default.rs create mode 100644 src/client/connection/response/failure/temporary/default/error.rs create mode 100644 src/client/connection/response/failure/temporary/proxy_error.rs create mode 100644 src/client/connection/response/failure/temporary/proxy_error/error.rs create mode 100644 src/client/connection/response/failure/temporary/server_unavailable.rs create mode 100644 src/client/connection/response/failure/temporary/server_unavailable/error.rs create mode 100644 src/client/connection/response/failure/temporary/slow_down.rs create mode 100644 src/client/connection/response/failure/temporary/slow_down/error.rs diff --git a/src/client/connection/response/failure.rs b/src/client/connection/response/failure.rs index 419af23..1ace0ed 100644 --- a/src/client/connection/response/failure.rs +++ b/src/client/connection/response/failure.rs @@ -38,10 +38,52 @@ impl Failure { // Getters + /// Get optional message for `Self` + /// * return `None` if the message is empty pub fn message(&self) -> Option<&str> { match self { Self::Permanent(permanent) => permanent.message(), Self::Temporary(temporary) => temporary.message(), } } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.message_or_default(), + Self::Temporary(temporary) => temporary.message_or_default(), + } + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Permanent(permanent) => permanent.as_str(), + Self::Temporary(temporary) => temporary.as_str(), + } + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Permanent(permanent) => permanent.as_bytes(), + Self::Temporary(temporary) => temporary.as_bytes(), + } + } +} + +#[test] +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Failure::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44, 50, 51, 52, 53, 59] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } diff --git a/src/client/connection/response/failure/temporary.rs b/src/client/connection/response/failure/temporary.rs index 768bdcd..cc20834 100644 --- a/src/client/connection/response/failure/temporary.rs +++ b/src/client/connection/response/failure/temporary.rs @@ -1,24 +1,31 @@ +pub mod cgi_error; +pub mod default; pub mod error; -pub use error::Error; +pub mod proxy_error; +pub mod server_unavailable; +pub mod slow_down; -const DEFAULT: (u8, &str) = (40, "Unspecified"); -const SERVER_UNAVAILABLE: (u8, &str) = (41, "Server unavailable"); -const CGI_ERROR: (u8, &str) = (42, "CGI error"); -const PROXY_ERROR: (u8, &str) = (43, "Proxy error"); -const SLOW_DOWN: (u8, &str) = (44, "Slow down"); +pub use cgi_error::CgiError; +pub use default::Default; +pub use error::Error; +pub use proxy_error::ProxyError; +pub use server_unavailable::ServerUnavailable; +pub use slow_down::SlowDown; + +const CODE: u8 = b'4'; /// https://geminiprotocol.net/docs/protocol-specification.gmi#temporary-failure pub enum Temporary { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-40 - Default { message: Option }, + Default(Default), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable - ServerUnavailable { message: Option }, + ServerUnavailable(ServerUnavailable), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error - CgiError { message: Option }, + CgiError(CgiError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error - ProxyError { message: Option }, + ProxyError(ProxyError), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down - SlowDown { message: Option }, + SlowDown(SlowDown), } impl Temporary { @@ -26,154 +33,94 @@ impl Temporary { /// 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)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::ServerUnavailable( + ServerUnavailable::from_utf8(buffer) + .map_err(Error::ServerUnavailable)?, + )), + b'2' => Ok(Self::CgiError( + CgiError::from_utf8(buffer).map_err(Error::CgiError)?, + )), + b'3' => Ok(Self::ProxyError( + ProxyError::from_utf8(buffer).map_err(Error::ProxyError)?, + )), + b'4' => Ok(Self::SlowDown( + SlowDown::from_utf8(buffer).map_err(Error::SlowDown)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Default { .. } => DEFAULT, - Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, - Self::CgiError { .. } => CGI_ERROR, - Self::ProxyError { .. } => PROXY_ERROR, - Self::SlowDown { .. } => SLOW_DOWN, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::ServerUnavailable { message } => message, - Self::CgiError { message } => message, - Self::ProxyError { message } => message, - Self::SlowDown { message } => message, + Self::Default(default) => default.message(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.message(), + Self::CgiError(cgi_error) => cgi_error.message(), + Self::ProxyError(proxy_error) => proxy_error.message(), + Self::SlowDown(slow_down) => slow_down.message(), } - .as_deref() } -} -impl std::fmt::Display for Temporary { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Default { .. } => DEFAULT, - Self::ServerUnavailable { .. } => SERVER_UNAVAILABLE, - Self::CgiError { .. } => CGI_ERROR, - Self::ProxyError { .. } => PROXY_ERROR, - Self::SlowDown { .. } => SLOW_DOWN, - } - .1 - ) + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return children `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + match self { + Self::Default(default) => default.message_or_default(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.message_or_default(), + Self::CgiError(cgi_error) => cgi_error.message_or_default(), + Self::ProxyError(proxy_error) => proxy_error.message_or_default(), + Self::SlowDown(slow_down) => slow_down.message_or_default(), + } } -} -impl std::str::FromStr for Temporary { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("40") { - return Ok(Self::Default { - message: message(postfix), - }); + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_str(), + Self::CgiError(cgi_error) => cgi_error.as_str(), + Self::ProxyError(proxy_error) => proxy_error.as_str(), + Self::SlowDown(slow_down) => slow_down.as_str(), } - if let Some(postfix) = header.strip_prefix("41") { - return Ok(Self::ServerUnavailable { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("42") { - return Ok(Self::CgiError { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("43") { - return Ok(Self::ProxyError { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("44") { - return Ok(Self::SlowDown { - message: message(postfix), - }); - } - Err(Error::Code) } -} -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::ServerUnavailable(server_unavailable) => server_unavailable.as_bytes(), + Self::CgiError(cgi_error) => cgi_error.as_bytes(), + Self::ProxyError(proxy_error) => proxy_error.as_bytes(), + Self::SlowDown(slow_down) => slow_down.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - - // 40 - let default = Temporary::from_str("40 Message\r\n").unwrap(); - assert_eq!(default.message(), Some("Message")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Temporary::from_str("40\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - // 41 - let server_unavailable = Temporary::from_str("41 Message\r\n").unwrap(); - assert_eq!(server_unavailable.message(), Some("Message")); - assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - let server_unavailable = Temporary::from_str("41\r\n").unwrap(); - assert_eq!(server_unavailable.message(), None); - assert_eq!(server_unavailable.to_code(), SERVER_UNAVAILABLE.0); - assert_eq!(server_unavailable.to_string(), SERVER_UNAVAILABLE.1); - - // 42 - let cgi_error = Temporary::from_str("42 Message\r\n").unwrap(); - assert_eq!(cgi_error.message(), Some("Message")); - assert_eq!(cgi_error.to_code(), CGI_ERROR.0); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - let cgi_error = Temporary::from_str("42\r\n").unwrap(); - assert_eq!(cgi_error.message(), None); - assert_eq!(cgi_error.to_code(), CGI_ERROR.0); - assert_eq!(cgi_error.to_string(), CGI_ERROR.1); - - // 43 - let proxy_error = Temporary::from_str("43 Message\r\n").unwrap(); - assert_eq!(proxy_error.message(), Some("Message")); - assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - let proxy_error = Temporary::from_str("43\r\n").unwrap(); - assert_eq!(proxy_error.message(), None); - assert_eq!(proxy_error.to_code(), PROXY_ERROR.0); - assert_eq!(proxy_error.to_string(), PROXY_ERROR.1); - - // 44 - let slow_down = Temporary::from_str("44 Message\r\n").unwrap(); - assert_eq!(slow_down.message(), Some("Message")); - assert_eq!(slow_down.to_code(), SLOW_DOWN.0); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); - - let slow_down = Temporary::from_str("44\r\n").unwrap(); - assert_eq!(slow_down.message(), None); - assert_eq!(slow_down.to_code(), SLOW_DOWN.0); - assert_eq!(slow_down.to_string(), SLOW_DOWN.1); +fn test() { + fn t(source: String, message: Option<&str>) { + let b = source.as_bytes(); + let i = Temporary::from_utf8(b).unwrap(); + assert_eq!(i.message(), message); + assert_eq!(i.as_str(), source); + assert_eq!(i.as_bytes(), b); + } + for code in [40, 41, 42, 43, 44] { + t(format!("{code} Message\r\n"), Some("Message")); + t(format!("{code}\r\n"), None); + } } diff --git a/src/client/connection/response/failure/temporary/cgi_error.rs b/src/client/connection/response/failure/temporary/cgi_error.rs new file mode 100644 index 0000000..8843fa9 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +pub const CODE: &[u8] = b"42"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "CGI Error"; + +/// Hold header `String` for [CGI Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-42-cgi-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct CgiError(String); + +impl CgiError { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let ce = CgiError::from_utf8("42 Message\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), Some("Message")); + assert_eq!(ce.message_or_default(), "Message"); + assert_eq!(ce.as_str(), "42 Message\r\n"); + assert_eq!(ce.as_bytes(), "42 Message\r\n".as_bytes()); + + let ce = CgiError::from_utf8("42\r\n".as_bytes()).unwrap(); + assert_eq!(ce.message(), None); + assert_eq!(ce.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(ce.as_str(), "42\r\n"); + assert_eq!(ce.as_bytes(), "42\r\n".as_bytes()); + + // err + assert!(CgiError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(CgiError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/cgi_error/error.rs b/src/client/connection/response/failure/temporary/cgi_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/cgi_error/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/default.rs b/src/client/connection/response/failure/temporary/default.rs new file mode 100644 index 0000000..e56d90b --- /dev/null +++ b/src/client/connection/response/failure/temporary/default.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code +pub const CODE: &[u8] = b"40"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Temporary error"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-40) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let d = Default::from_utf8("40 Message\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), Some("Message")); + assert_eq!(d.message_or_default(), "Message"); + assert_eq!(d.as_str(), "40 Message\r\n"); + assert_eq!(d.as_bytes(), "40 Message\r\n".as_bytes()); + + let d = Default::from_utf8("40\r\n".as_bytes()).unwrap(); + assert_eq!(d.message(), None); + assert_eq!(d.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(d.as_str(), "40\r\n"); + assert_eq!(d.as_bytes(), "40\r\n".as_bytes()); + + // err + assert!(Default::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/default/error.rs b/src/client/connection/response/failure/temporary/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/error.rs b/src/client/connection/response/failure/temporary/error.rs index 5cf1cf6..afa9154 100644 --- a/src/client/connection/response/failure/temporary/error.rs +++ b/src/client/connection/response/failure/temporary/error.rs @@ -1,22 +1,47 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + CgiError(super::cgi_error::Error), + Default(super::default::Error), + FirstByte(u8), + ProxyError(super::proxy_error::Error), + SecondByte(u8), + ServerUnavailable(super::server_unavailable::Error), + SlowDown(super::slow_down::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::CgiError(e) => { + write!(f, "CgiError parse error: {e}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") + } + Self::ProxyError(e) => { + write!(f, "ProxyError parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::ServerUnavailable(e) => { + write!(f, "ServerUnavailable parse error: {e}") + } + Self::SlowDown(e) => { + write!(f, "SlowDown parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/failure/temporary/proxy_error.rs b/src/client/connection/response/failure/temporary/proxy_error.rs new file mode 100644 index 0000000..1264c34 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error.rs @@ -0,0 +1,78 @@ +pub mod error; +pub use error::Error; + +/// [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +pub const CODE: &[u8] = b"43"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Proxy error"; + +/// Hold header `String` for [Proxy Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-43-proxy-error) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ProxyError(String); + +impl ProxyError { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let pe = ProxyError::from_utf8("43 Message\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), Some("Message")); + assert_eq!(pe.message_or_default(), "Message"); + assert_eq!(pe.as_str(), "43 Message\r\n"); + assert_eq!(pe.as_bytes(), "43 Message\r\n".as_bytes()); + + let pe = ProxyError::from_utf8("43\r\n".as_bytes()).unwrap(); + assert_eq!(pe.message(), None); + assert_eq!(pe.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(pe.as_str(), "43\r\n"); + assert_eq!(pe.as_bytes(), "43\r\n".as_bytes()); + + // err + assert!(ProxyError::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ProxyError::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/proxy_error/error.rs b/src/client/connection/response/failure/temporary/proxy_error/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/proxy_error/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/server_unavailable.rs b/src/client/connection/response/failure/temporary/server_unavailable.rs new file mode 100644 index 0000000..f42802e --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +pub const CODE: &[u8] = b"41"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Server unavailable"; + +/// Hold header `String` for [Server Unavailable](https://geminiprotocol.net/docs/protocol-specification.gmi#status-41-server-unavailable) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct ServerUnavailable(String); + +impl ServerUnavailable { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let su = ServerUnavailable::from_utf8("41 Message\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), Some("Message")); + assert_eq!(su.message_or_default(), "Message"); + assert_eq!(su.as_str(), "41 Message\r\n"); + assert_eq!(su.as_bytes(), "41 Message\r\n".as_bytes()); + + let su = ServerUnavailable::from_utf8("41\r\n".as_bytes()).unwrap(); + assert_eq!(su.message(), None); + assert_eq!(su.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(su.as_str(), "41\r\n"); + assert_eq!(su.as_bytes(), "41\r\n".as_bytes()); + + // err + assert!(ServerUnavailable::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(ServerUnavailable::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/server_unavailable/error.rs b/src/client/connection/response/failure/temporary/server_unavailable/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/server_unavailable/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/failure/temporary/slow_down.rs b/src/client/connection/response/failure/temporary/slow_down.rs new file mode 100644 index 0000000..3ca346d --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down.rs @@ -0,0 +1,81 @@ +pub mod error; +pub use error::Error; + +/// [Slow Down](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +pub const CODE: &[u8] = b"44"; + +/// Default message if the optional value was not provided by the server +/// * useful to skip match cases in external applications, +/// by using `super::message_or_default` method. +pub const DEFAULT_MESSAGE: &str = "Slow down"; + +/// Hold header `String` for [Unspecified Temporary Error](https://geminiprotocol.net/docs/protocol-specification.gmi#status-44-slow-down) +/// temporary error status code +/// +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct SlowDown(String); + +impl SlowDown { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + /// Get optional message for `Self` + /// * if the optional message not provided by the server, return `DEFAULT_MESSAGE` + pub fn message_or_default(&self) -> &str { + self.message().unwrap_or(DEFAULT_MESSAGE) + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get header bytes of `Self` + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let sd = SlowDown::from_utf8("44 Message\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), Some("Message")); + assert_eq!(sd.message_or_default(), "Message"); + assert_eq!(sd.as_str(), "44 Message\r\n"); + assert_eq!(sd.as_bytes(), "44 Message\r\n".as_bytes()); + + let sd = SlowDown::from_utf8("44\r\n".as_bytes()).unwrap(); + assert_eq!(sd.message(), None); + assert_eq!(sd.message_or_default(), DEFAULT_MESSAGE); + assert_eq!(sd.as_str(), "44\r\n"); + assert_eq!(sd.as_bytes(), "44\r\n".as_bytes()); + + // err + assert!(SlowDown::from_utf8("13 Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(SlowDown::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/failure/temporary/slow_down/error.rs b/src/client/connection/response/failure/temporary/slow_down/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/failure/temporary/slow_down/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +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}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} From 46da3a031af584bc235a0942cb07493b38022222 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 07:08:20 +0200 Subject: [PATCH 78/95] remove extras --- src/client/connection/response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/connection/response.rs b/src/client/connection/response.rs index 840cf08..7dd54bb 100644 --- a/src/client/connection/response.rs +++ b/src/client/connection/response.rs @@ -74,7 +74,7 @@ impl Response { connection, ) }, - ); + ) } } From 8ee088270f5ba128a7dd766e57490f1778148501 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 07:36:48 +0200 Subject: [PATCH 79/95] implement high-level getters, add comments, improve tests --- src/client/connection/response/success.rs | 47 +++++++++++++++++-- .../connection/response/success/default.rs | 23 ++++++--- .../response/success/default/header.rs | 41 ++++++++++------ 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/src/client/connection/response/success.rs b/src/client/connection/response/success.rs index ecee769..f9493d6 100644 --- a/src/client/connection/response/success.rs +++ b/src/client/connection/response/success.rs @@ -24,14 +24,53 @@ impl Success { Err(e) => Err(Error::Default(e)), } } + + // Getters + + /// Get header bytes for `Self` type + pub fn as_header_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.header.as_bytes(), + } + } + + /// Get header string for `Self` type + pub fn as_header_str(&self) -> &str { + match self { + Self::Default(default) => default.header.as_str(), + } + } + + /// Get parsed MIME for `Self` type + /// + /// * high-level method, useful to skip extra match case constructions; + /// * at this moment, Gemini protocol has only one status code in this scope,\ + /// this method would be deprecated in future, use on your own risk! + pub fn mime(&self) -> Result { + match self { + Self::Default(default) => default + .header + .mime() + .map_err(|e| Error::Default(default::Error::Header(e))), + } + } } #[test] fn test() { - match Success::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap() { - Success::Default(default) => { - assert_eq!(default.header.mime().unwrap(), "text/gemini"); - assert_eq!(default.content, None) + let r = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = r.as_bytes(); + let s = Success::from_utf8(b).unwrap(); + + match s { + Success::Default(ref d) => { + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()) } } + assert_eq!(s.as_header_bytes(), b); + assert_eq!(s.as_header_str(), r); + assert_eq!(s.mime().unwrap(), "text/gemini"); + + assert!(Success::from_utf8("40 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()) } diff --git a/src/client/connection/response/success/default.rs b/src/client/connection/response/success/default.rs index a905318..488c3e6 100644 --- a/src/client/connection/response/success/default.rs +++ b/src/client/connection/response/success/default.rs @@ -11,13 +11,18 @@ pub const CODE: &[u8] = b"20"; /// * this response type MAY contain body data /// * the header has closed members to require valid construction pub struct Default { + /// Formatted header holder with additional API pub header: Header, - pub content: Option>, + /// Default success response MAY include body data + /// * if the `Request` constructed with `Mode::HeaderOnly` flag,\ + /// this value wants to be processed manually, using external application logic (specific for content-type) + pub content: Vec, } impl Default { // Constructors + /// Parse `Self` from buffer contains header bytes pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(CODE) { return Err(Error::Code); @@ -25,9 +30,9 @@ impl Default { let header = Header::from_utf8(buffer).map_err(Error::Header)?; Ok(Self { content: buffer - .get(header.len() + 1..) + .get(header.as_bytes().len()..) .filter(|s| !s.is_empty()) - .map(|v| v.to_vec()), + .map_or(Vec::new(), |v| v.to_vec()), header, }) } @@ -35,8 +40,12 @@ impl Default { #[test] fn test() { - let default = - Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); - assert_eq!(default.header.mime().unwrap(), "text/gemini"); - assert_eq!(default.content, None) + let d = Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert!(d.content.is_empty()); + + let d = + Default::from_utf8("20 text/gemini; charset=utf-8; lang=en\r\ndata".as_bytes()).unwrap(); + assert_eq!(d.header.mime().unwrap(), "text/gemini"); + assert_eq!(d.content.len(), 4); } diff --git a/src/client/connection/response/success/default/header.rs b/src/client/connection/response/success/default/header.rs index 929cb96..dab58b7 100644 --- a/src/client/connection/response/success/default/header.rs +++ b/src/client/connection/response/success/default/header.rs @@ -1,19 +1,22 @@ pub mod error; pub use error::Error; -pub struct Header(Vec); +pub struct Header(String); impl Header { // Constructors + /// Parse `Self` from buffer contains header bytes pub fn from_utf8(buffer: &[u8]) -> Result { if !buffer.starts_with(super::CODE) { return Err(Error::Code); } Ok(Self( - crate::client::connection::response::header_bytes(buffer) - .map_err(Error::Header)? - .to_vec(), + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), )) } @@ -23,7 +26,7 @@ impl Header { pub fn mime(&self) -> Result { glib::Regex::split_simple( r"^\d{2}\s([^\/]+\/[^\s;]+)", - std::str::from_utf8(&self.0).map_err(Error::Utf8Error)?, + &self.0, glib::RegexCompileFlags::DEFAULT, glib::RegexMatchFlags::DEFAULT, ) @@ -33,15 +36,25 @@ impl Header { .map_or(Err(Error::Mime), |s| Ok(s.to_lowercase())) } - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - + /// Get header bytes of `Self` pub fn as_bytes(&self) -> &[u8] { - &self.0 + self.0.as_bytes() + } + + /// Get header string of `Self` + pub fn as_str(&self) -> &str { + self.0.as_str() } } + +#[test] +fn test() { + let s = "20 text/gemini; charset=utf-8; lang=en\r\n"; + let b = s.as_bytes(); + let h = Header::from_utf8(b).unwrap(); + assert_eq!(h.mime().unwrap(), "text/gemini"); + assert_eq!(h.as_bytes(), b); + assert_eq!(h.as_str(), s); + + assert!(Header::from_utf8("21 text/gemini; charset=utf-8; lang=en\r\n".as_bytes()).is_err()); +} From b6ea830545b6da6cb073cfb37a2bd6260e5c1622 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 25 Mar 2025 07:38:54 +0200 Subject: [PATCH 80/95] update example --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3abf80f..d81850e 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,9 @@ fn main() -> ExitCode { None, // optional `GTlsCertificate` |result| match result { Ok((response, _connection)) => match response { - Response::Success(success) => match success { - Success::Default(default) => match default.header.mime().unwrap().as_str() { - "text/gemini" => todo!(), - _ => todo!(), - } + Response::Success(success) => match success.mime().unwrap().as_str() { + "text/gemini" => todo!(), + _ => todo!(), }, _ => todo!(), }, From 4dddbd5f8a0a5160690ede5997c4e22f36767721 Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 27 Mar 2025 21:14:14 +0200 Subject: [PATCH 81/95] make Size struct for tuple argument --- src/gio/file_output_stream.rs | 57 +++++++++++++++++------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 7d9415c..c915b2f 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -7,6 +7,14 @@ use gio::{ }; use glib::{Bytes, Priority, object::IsA}; +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + /// `None` for unlimited + pub limit: Option, + pub total: usize, +} + /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread @@ -15,59 +23,50 @@ pub fn from_stream_async( file_output_stream: FileOutputStream, cancellable: Cancellable, priority: Priority, - (chunk, limit, mut total): ( - usize, // bytes_in_chunk - Option, // bytes_total_limit, `None` to unlimited - usize, // bytes_total - ), + mut size: Size, (on_chunk, on_complete): ( impl Fn(Bytes, usize) + 'static, // on_chunk impl FnOnce(Result<(FileOutputStream, usize), Error>) + 'static, // on_complete ), ) { io_stream.input_stream().read_bytes_async( - chunk, + size.chunk, priority, Some(&cancellable.clone()), move |result| match result { Ok(bytes) => { - total += bytes.len(); - on_chunk(bytes.clone(), total); + size.total += bytes.len(); + on_chunk(bytes.clone(), size.total); - if let Some(limit) = limit { - if total > limit { - return on_complete(Err(Error::BytesTotal(total, limit))); + if let Some(limit) = size.limit { + if size.total > limit { + return on_complete(Err(Error::BytesTotal(size.total, limit))); } } if bytes.len() == 0 { - return on_complete(Ok((file_output_stream, total))); + return on_complete(Ok((file_output_stream, size.total))); } // Make sure **all 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 file_output_stream.clone().write_all_async( - bytes.clone(), + bytes, priority, Some(&cancellable.clone()), - move |result| { - match result { - Ok(_) => { - // continue read.. - from_stream_async( - io_stream, - file_output_stream, - cancellable, - priority, - (chunk, limit, total), - (on_chunk, on_complete), - ); - } - Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), - } + move |result| match result { + Ok(_) => from_stream_async( + io_stream, + file_output_stream, + cancellable, + priority, + size, + (on_chunk, on_complete), + ), + Err((b, e)) => on_complete(Err(Error::OutputStream(b, e))), }, - ); + ) } Err(e) => on_complete(Err(Error::InputStream(e))), }, From 9bbaecf344440496da4873b6988ce823aebc656e Mon Sep 17 00:00:00 2001 From: yggverse Date: Thu, 27 Mar 2025 21:33:50 +0200 Subject: [PATCH 82/95] make Size struct for tuple argument, move Size to separated mod --- src/gio/file_output_stream.rs | 11 +++-------- src/gio/file_output_stream/size.rs | 7 +++++++ src/gio/memory_input_stream.rs | 27 +++++++++++++++++---------- src/gio/memory_input_stream/size.rs | 6 ++++++ 4 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 src/gio/file_output_stream/size.rs create mode 100644 src/gio/memory_input_stream/size.rs diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index c915b2f..a8e5d70 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -1,5 +1,8 @@ pub mod error; +pub mod size; + pub use error::Error; +pub use size::Size; use gio::{ Cancellable, FileOutputStream, IOStream, @@ -7,14 +10,6 @@ use gio::{ }; use glib::{Bytes, Priority, object::IsA}; -/// Mutable bytes count -pub struct Size { - pub chunk: usize, - /// `None` for unlimited - pub limit: Option, - pub total: usize, -} - /// Asynchronously move all bytes from [IOStream](https://docs.gtk.org/gio/class.IOStream.html) /// to [FileOutputStream](https://docs.gtk.org/gio/class.FileOutputStream.html) /// * require `IOStream` reference to keep `Connection` active in async thread diff --git a/src/gio/file_output_stream/size.rs b/src/gio/file_output_stream/size.rs new file mode 100644 index 0000000..285d9f1 --- /dev/null +++ b/src/gio/file_output_stream/size.rs @@ -0,0 +1,7 @@ +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + /// `None` for unlimited + pub limit: Option, + pub total: usize, +} diff --git a/src/gio/memory_input_stream.rs b/src/gio/memory_input_stream.rs index 21c6337..2b1fc39 100644 --- a/src/gio/memory_input_stream.rs +++ b/src/gio/memory_input_stream.rs @@ -1,5 +1,8 @@ pub mod error; +pub mod size; + pub use error::Error; +pub use size::Size; use gio::{ Cancellable, IOStream, MemoryInputStream, @@ -17,7 +20,7 @@ pub fn from_stream_async( io_stream: impl IsA, priority: Priority, cancelable: Cancellable, - (chunk, limit): (usize, usize), + size: Size, (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, @@ -28,7 +31,7 @@ pub fn from_stream_async( io_stream, priority, cancelable, - (chunk, limit, 0), + size, (on_chunk, on_complete), ); } @@ -41,14 +44,14 @@ pub fn for_memory_input_stream_async( io_stream: impl IsA, priority: Priority, cancellable: Cancellable, - (chunk, limit, mut total): (usize, usize, usize), + mut size: Size, (on_chunk, on_complete): ( impl Fn(usize, usize) + 'static, impl FnOnce(Result<(MemoryInputStream, usize), Error>) + 'static, ), ) { io_stream.input_stream().read_bytes_async( - chunk, + size.chunk, priority, Some(&cancellable.clone()), move |result| match result { @@ -57,19 +60,23 @@ pub fn for_memory_input_stream_async( // is end of stream if len == 0 { - return on_complete(Ok((memory_input_stream, total))); + return on_complete(Ok((memory_input_stream, size.total))); } // callback chunk function - total += len; - on_chunk(len, total); + size.total += len; + on_chunk(len, size.total); // push bytes into the memory pool memory_input_stream.add_bytes(&bytes); // prevent memory overflow - if total > limit { - return on_complete(Err(Error::BytesTotal(memory_input_stream, total, limit))); + if size.total > size.limit { + return on_complete(Err(Error::BytesTotal( + memory_input_stream, + size.total, + size.limit, + ))); } // handle next chunk.. @@ -78,7 +85,7 @@ pub fn for_memory_input_stream_async( io_stream, priority, cancellable, - (chunk, limit, total), + size, (on_chunk, on_complete), ) } diff --git a/src/gio/memory_input_stream/size.rs b/src/gio/memory_input_stream/size.rs new file mode 100644 index 0000000..b95ef39 --- /dev/null +++ b/src/gio/memory_input_stream/size.rs @@ -0,0 +1,6 @@ +/// Mutable bytes count +pub struct Size { + pub chunk: usize, + pub limit: usize, + pub total: usize, +} From c79f386bf1c1c3e5eb02920adf143122358a7e01 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 28 Mar 2025 00:33:33 +0200 Subject: [PATCH 83/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 442d0ee..27d8051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.18.0" +version = "0.18.1" edition = "2024" license = "MIT" readme = "README.md" From bb5b1dfb533ded64b0eb02d1cdecb19bbd7d40a9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 08:44:50 +0300 Subject: [PATCH 84/95] apply clippy optimizations --- src/gio/file_output_stream.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index a8e5d70..777600f 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -39,7 +39,7 @@ pub fn from_stream_async( } } - if bytes.len() == 0 { + if bytes.is_empty() { return on_complete(Ok((file_output_stream, size.total))); } From 44196608cebe51213c1feb4cc53bc8557bc16f72 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 08:48:56 +0300 Subject: [PATCH 85/95] implement optional TOFU validation --- Cargo.toml | 2 +- README.md | 3 ++- src/client.rs | 6 ++++-- src/client/connection.rs | 27 +++++++++++++++++++-------- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 27d8051..1c6ea42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.18.1" +version = "0.19.0" edition = "2024" license = "MIT" readme = "README.md" diff --git a/README.md b/README.md index d81850e..03c633e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ fn main() -> ExitCode { }, Priority::DEFAULT, Cancellable::new(), - None, // optional `GTlsCertificate` + None, // optional auth `GTlsCertificate` + None, // optional TOFU `GTlsCertificate` array |result| match result { Ok((response, _connection)) => match response { Response::Success(success) => match success.mime().unwrap().as_str() { diff --git a/src/client.rs b/src/client.rs index cff557e..f11e9ec 100644 --- a/src/client.rs +++ b/src/client.rs @@ -59,7 +59,8 @@ impl Client { request: Request, priority: Priority, cancellable: Cancellable, - certificate: Option, + client_certificate: Option, + server_certificates: Option>, callback: impl FnOnce(Result<(Response, Connection), Error>) + 'static, ) { // Begin new connection @@ -75,7 +76,8 @@ impl Client { match Connection::build( socket_connection, network_address, - certificate, + client_certificate, + server_certificates, is_session_resumption, ) { Ok(connection) => connection.request_async( diff --git a/src/client/connection.rs b/src/client/connection.rs index d1cd849..b7833a7 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -6,11 +6,9 @@ pub use error::Error; pub use request::{Mode, Request}; pub use response::Response; -// Local dependencies - use gio::{ Cancellable, IOStream, NetworkAddress, SocketConnection, TlsCertificate, TlsClientConnection, - prelude::{IOStreamExt, OutputStreamExtManual, TlsConnectionExt}, + prelude::{IOStreamExt, OutputStreamExtManual, TlsCertificateExt, TlsConnectionExt}, }; use glib::{ Bytes, Priority, @@ -30,17 +28,19 @@ impl Connection { pub fn build( socket_connection: SocketConnection, network_address: NetworkAddress, - certificate: Option, + client_certificate: Option, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { Ok(Self { tls_client_connection: match new_tls_client_connection( &socket_connection, Some(&network_address), + server_certificates, is_session_resumption, ) { Ok(tls_client_connection) => { - if let Some(ref c) = certificate { + if let Some(ref c) = client_certificate { tls_client_connection.set_certificate(c); } tls_client_connection @@ -136,6 +136,7 @@ impl Connection { fn new_tls_client_connection( socket_connection: &SocketConnection, server_identity: Option<&NetworkAddress>, + server_certificates: Option>, is_session_resumption: bool, ) -> Result { match TlsClientConnection::new(socket_connection, server_identity) { @@ -149,9 +150,19 @@ fn new_tls_client_connection( // https://geminiprotocol.net/docs/protocol-specification.gmi#closing-connections tls_client_connection.set_require_close_notify(true); - // @TODO validate - // https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation - tls_client_connection.connect_accept_certificate(|_, _, _| true); + // [TOFU](https://geminiprotocol.net/docs/protocol-specification.gmi#tls-server-certificate-validation) + tls_client_connection.connect_accept_certificate(move |_, c, _| { + server_certificates + .as_ref() + .is_none_or(|server_certificates| { + for server_certificate in server_certificates { + if server_certificate.is_same(c) { + return true; + } + } + false + }) + }); Ok(tls_client_connection) } From cc1018224a42b3d63e9cc77db86102710e79435a Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 09:46:32 +0300 Subject: [PATCH 86/95] reorganize error types, return `socket_connection` on init error --- src/client.rs | 10 ++++++---- src/client/error.rs | 16 ++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/client.rs b/src/client.rs index f11e9ec..0f33a94 100644 --- a/src/client.rs +++ b/src/client.rs @@ -74,7 +74,7 @@ impl Client { move |result| match result { Ok(socket_connection) => { match Connection::build( - socket_connection, + socket_connection.clone(), network_address, client_certificate, server_certificates, @@ -87,18 +87,20 @@ impl Client { move |result| { callback(match result { Ok(response) => Ok(response), - Err(e) => Err(Error::Connection(e)), + Err(e) => Err(Error::Request(e)), }) }, ), - Err(e) => callback(Err(Error::Connection(e))), + Err(e) => { + callback(Err(Error::Connection(socket_connection, e))) + } } } Err(e) => callback(Err(Error::Connect(e))), } }) } - Err(e) => callback(Err(Error::Request(e))), + Err(e) => callback(Err(Error::NetworkAddress(e))), } } diff --git a/src/client/error.rs b/src/client/error.rs index 6083e77..b49d65a 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -3,21 +3,25 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { Connect(glib::Error), - Connection(crate::client::connection::Error), - Request(crate::client::connection::request::Error), + Connection(gio::SocketConnection, crate::client::connection::Error), + NetworkAddress(crate::client::connection::request::Error), + Request(crate::client::connection::Error), } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connection(e) => { - write!(f, "Connection error: {e}") - } Self::Connect(e) => { write!(f, "Connect error: {e}") } + Self::Connection(_, e) => { + write!(f, "Connection init error: {e}") + } + Self::NetworkAddress(e) => { + write!(f, "Network address error: {e}") + } Self::Request(e) => { - write!(f, "Request error: {e}") + write!(f, "Connection error: {e}") } } } From e878fe4ba2d7b73816dd3e4be90411aa401ab272 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 22 Jul 2025 09:49:39 +0300 Subject: [PATCH 87/95] return `NetworkAddress` on `Error::Connect` --- src/client.rs | 2 +- src/client/error.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.rs b/src/client.rs index 0f33a94..5198f81 100644 --- a/src/client.rs +++ b/src/client.rs @@ -96,7 +96,7 @@ impl Client { } } } - Err(e) => callback(Err(Error::Connect(e))), + Err(e) => callback(Err(Error::Connect(network_address, e))), } }) } diff --git a/src/client/error.rs b/src/client/error.rs index b49d65a..eb951b6 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Connect(glib::Error), + Connect(gio::NetworkAddress, glib::Error), Connection(gio::SocketConnection, crate::client::connection::Error), NetworkAddress(crate::client::connection::request::Error), Request(crate::client::connection::Error), @@ -11,7 +11,7 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Connect(e) => { + Self::Connect(_, e) => { write!(f, "Connect error: {e}") } Self::Connection(_, e) => { From c5d10e020a1d246aa1bd5ae521cde03f06155e6d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 01:27:20 +0300 Subject: [PATCH 88/95] return `Connection` on `Request` error --- src/client.rs | 4 ++-- src/client/connection.rs | 1 + src/client/error.rs | 7 +++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/client.rs b/src/client.rs index 5198f81..2152781 100644 --- a/src/client.rs +++ b/src/client.rs @@ -80,14 +80,14 @@ impl Client { server_certificates, is_session_resumption, ) { - Ok(connection) => connection.request_async( + Ok(connection) => connection.clone().request_async( request, priority, cancellable, move |result| { callback(match result { Ok(response) => Ok(response), - Err(e) => Err(Error::Request(e)), + Err(e) => Err(Error::Request(connection, e)), }) }, ), diff --git a/src/client/connection.rs b/src/client/connection.rs index b7833a7..6be90f1 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -15,6 +15,7 @@ use glib::{ object::{Cast, ObjectExt}, }; +#[derive(Debug, Clone)] pub struct Connection { pub network_address: NetworkAddress, pub socket_connection: SocketConnection, diff --git a/src/client/error.rs b/src/client/error.rs index eb951b6..73031da 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -5,7 +5,10 @@ pub enum Error { Connect(gio::NetworkAddress, glib::Error), Connection(gio::SocketConnection, crate::client::connection::Error), NetworkAddress(crate::client::connection::request::Error), - Request(crate::client::connection::Error), + Request( + crate::client::connection::Connection, + crate::client::connection::Error, + ), } impl Display for Error { @@ -20,7 +23,7 @@ impl Display for Error { Self::NetworkAddress(e) => { write!(f, "Network address error: {e}") } - Self::Request(e) => { + Self::Request(_, e) => { write!(f, "Connection error: {e}") } } From d8e0a8e35a136ee4b32e3bb1c38979539c0d8044 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 03:07:24 +0300 Subject: [PATCH 89/95] update dependencies version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1c6ea42..7c12b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.9" +version = "0.21" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.9" +version = "0.21" features = ["v2_66"] From 5019e6666772b8d06823339176094db9062167f9 Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 04:29:01 +0300 Subject: [PATCH 90/95] use latest 0.20 api --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7c12b4a..0e27134 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.21" +version = "0.20.12" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.21" +version = "0.20.12" features = ["v2_66"] From f8537e4ab63cf07992f12c909ea9480d541df35d Mon Sep 17 00:00:00 2001 From: yggverse Date: Wed, 23 Jul 2025 05:08:13 +0300 Subject: [PATCH 91/95] use latest dependencies version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0e27134..901e1b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ repository = "https://github.com/YGGverse/ggemini" [dependencies.gio] package = "gio" -version = "0.20.12" +version = "0.21.0" features = ["v2_70"] [dependencies.glib] package = "glib" -version = "0.20.12" +version = "0.21.0" features = ["v2_66"] From 7e9ecf64b3c5e456f739413892d14b0c5c83dda2 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Oct 2025 22:37:48 +0300 Subject: [PATCH 92/95] implement default trait --- src/gio/file_output_stream/size.rs | 10 ++++++++++ src/gio/memory_input_stream/size.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/gio/file_output_stream/size.rs b/src/gio/file_output_stream/size.rs index 285d9f1..5d0c911 100644 --- a/src/gio/file_output_stream/size.rs +++ b/src/gio/file_output_stream/size.rs @@ -5,3 +5,13 @@ pub struct Size { pub limit: Option, pub total: usize, } + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: None, + total: 0, + } + } +} diff --git a/src/gio/memory_input_stream/size.rs b/src/gio/memory_input_stream/size.rs index b95ef39..9a10bd3 100644 --- a/src/gio/memory_input_stream/size.rs +++ b/src/gio/memory_input_stream/size.rs @@ -4,3 +4,13 @@ pub struct Size { pub limit: usize, pub total: usize, } + +impl Default for Size { + fn default() -> Self { + Self { + chunk: 0x10000, // 64KB + limit: 0xfffff, // 1 MB + total: 0, + } + } +} From 0f6eaa563c428f85ecd49c9f562b2987b6fed317 Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Oct 2025 22:38:51 +0300 Subject: [PATCH 93/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 901e1b7..12c3c6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.19.0" +version = "0.20.0" edition = "2024" license = "MIT" readme = "README.md" From bba51e38e831cc84060c901e1dbe2e6efa0823ad Mon Sep 17 00:00:00 2001 From: yggverse Date: Sun, 19 Oct 2025 22:46:49 +0300 Subject: [PATCH 94/95] apply fmt updates --- src/gio/file_output_stream.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gio/file_output_stream.rs b/src/gio/file_output_stream.rs index 777600f..2dffb5e 100644 --- a/src/gio/file_output_stream.rs +++ b/src/gio/file_output_stream.rs @@ -33,10 +33,10 @@ pub fn from_stream_async( size.total += bytes.len(); on_chunk(bytes.clone(), size.total); - if let Some(limit) = size.limit { - if size.total > limit { - return on_complete(Err(Error::BytesTotal(size.total, limit))); - } + if let Some(limit) = size.limit + && size.total > limit + { + return on_complete(Err(Error::BytesTotal(size.total, limit))); } if bytes.is_empty() { From 11d17e004e473354bc0d9ca9ae701af6cd281ee7 Mon Sep 17 00:00:00 2001 From: yggverse Date: Fri, 7 Nov 2025 21:15:20 +0200 Subject: [PATCH 95/95] update version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 12c3c6c..801fb2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ggemini" -version = "0.20.0" +version = "0.20.1" edition = "2024" license = "MIT" readme = "README.md"